diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 007c28c..37893f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,31 @@ +variables: + GIT_SUBMODULE_STRATEGY: recursive + stages: - build -include: - - template: Jobs/Build.gitlab-ci.yml +services: + - docker:24.0.5-dind + +before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + +build: + stage: build + image: docker:latest + variables: + IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA + LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest + PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest + script: + - docker build -t $IMAGE_NAME . + - docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME + - docker push $IMAGE_NAME + - docker push $LATEST_IMAGE_NAME + - | + if [ $CI_COMMIT_TAG ]; then + docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME + docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME + docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME + fi diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e24bb0c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "drop-base"] + path = drop-base + url = https://github.com/Drop-OSS/drop-base.git diff --git a/.vscode/settings.json b/.vscode/settings.json index 42e000e..87ca8ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,17 @@ "spellchecker.ignoreWordsList": [ "mTLS", "Wireguard" + ], + "sqltools.connections": [ + { + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "driver": "PostgreSQL", + "name": "drop", + "database": "drop", + "username": "drop", + "password": "drop" + } ] -} \ No newline at end of file +} diff --git a/app.vue b/app.vue index b34046f..5b6668f 100644 --- a/app.vue +++ b/app.vue @@ -2,6 +2,7 @@ + diff --git a/components/CarouselPagination.vue b/components/CarouselPagination.vue index b4aa338..6ec24ae 100644 --- a/components/CarouselPagination.vue +++ b/components/CarouselPagination.vue @@ -1,11 +1,11 @@ - + slideTo(i)" :class="[ - currentSlide == i ? 'bg-zinc-300' : 'bg-zinc-700', - 'cursor-pointer w-4 h-2 rounded-full', + currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3', + 'transition-all cursor-pointer h-2 rounded-full', ]" /> diff --git a/components/GamePanel.vue b/components/GamePanel.vue index 8ad2a87..be6104d 100644 --- a/components/GamePanel.vue +++ b/components/GamePanel.vue @@ -6,7 +6,7 @@ > diff --git a/components/LoadingButton.vue b/components/LoadingButton.vue deleted file mode 100644 index b61303f..0000000 --- a/components/LoadingButton.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - Loading... - - - - - - diff --git a/components/UploadFileDialog.vue b/components/UploadFileDialog.vue index f44bc6b..fd1dd77 100644 --- a/components/UploadFileDialog.vue +++ b/components/UploadFileDialog.vue @@ -17,7 +17,7 @@ uploadFile_wrapper()" :class="[ 'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto', - currentFile === undefined - ? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10' - : 'text-white bg-blue-600 hover:bg-blue-500', ]" > Upload @@ -121,8 +118,8 @@ import { TransitionChild, TransitionRoot, } from "@headlessui/vue"; -import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline"; import { ArrowUpTrayIcon } from "@heroicons/vue/20/solid"; +import { XCircleIcon } from "@heroicons/vue/24/solid"; const open = defineModel(); diff --git a/composables/collection.ts b/composables/collection.ts new file mode 100644 index 0000000..e69de29 diff --git a/composables/icons.ts b/composables/icons.ts new file mode 100644 index 0000000..ca465ab --- /dev/null +++ b/composables/icons.ts @@ -0,0 +1,6 @@ +import { IconsLinuxLogo, IconsWindowsLogo } from "#components"; + +export const PLATFORM_ICONS = { + [PlatformClient.Linux]: IconsLinuxLogo, + [PlatformClient.Windows]: IconsWindowsLogo, +}; diff --git a/composables/task.ts b/composables/task.ts index 7a0c8f2..77515ef 100644 --- a/composables/task.ts +++ b/composables/task.ts @@ -2,45 +2,74 @@ import type { TaskMessage } from "~/server/internal/tasks"; import { WebSocketHandler } from "./ws"; const websocketHandler = new WebSocketHandler("/api/v1/task"); +const taskStates: { [key: string]: Ref } = {}; + +function handleUpdateMessage(msg: TaskMessage) { + const taskStates = useTaskStates(); + const state = taskStates[msg.id]; + if (!state) return; + if (!state.value || msg.reset) { + state.value = msg; + return; + } + state.value.log.push(...msg.log); + + Object.assign(state.value, { ...msg, log: state.value.log }); +} websocketHandler.listen((message) => { - const msg = JSON.parse(message) as TaskMessage; - const taskStates = useTaskStates(); - const state = taskStates.value[msg.id]; - if (!state) return; - state.value = msg; + try { + // If it's an object, it's an update message + const msg = JSON.parse(message) as TaskMessage; + handleUpdateMessage(msg); + } catch { + // Otherwise it's control message + const taskStates = useTaskStates(); + + const [action, ...data] = message.split("/"); + + switch (action) { + case "connect": + const taskReady = useTaskReady(); + taskReady.value = true; + break; + case "disconnect": + const disconnectTaskId = data[0]; + delete taskStates[disconnectTaskId]; + console.log(`disconnected from ${disconnectTaskId}`); + break; + case "error": + const [taskId, title, description] = data; + taskStates[taskId].value ??= { + id: taskId, + name: "Unknown task", + success: false, + progress: 0, + error: undefined, + log: [], + }; + taskStates[taskId].value.error = { title, description }; + break; + } + } }); -const useTaskStates = () => - useState<{ [key: string]: Ref }>("task-states", () => ({ - connect: useState("task-connect", () => ({ - id: "connect", - name: "Connect", - success: false, - progress: 0, - log: [], - error: undefined, - })), - })); +const useTaskStates = () => taskStates; -export const useTaskReady = () => { +export const useTaskReady = () => useState("taskready", () => false); + +export const useTask = (taskId: string): Ref => { + if (import.meta.server) return ref(undefined); const taskStates = useTaskStates(); - return taskStates.value["connect"]; -}; + if ( + taskStates[taskId] && + taskStates[taskId].value && + !taskStates[taskId].value.error + ) + return taskStates[taskId]; -export const useTask = (taskId: string): Ref => { - if (import.meta.server) return {} as unknown as Ref; - const taskStates = useTaskStates(); - if (taskStates.value[taskId]) return taskStates.value[taskId]; - - taskStates.value[taskId] = useState(`task-${taskId}`, () => ({ - id: taskId, - name: "loading...", - success: false, - progress: 0, - error: undefined, - log: [], - })); + taskStates[taskId] = ref(undefined); + console.log("connecting to " + taskId); websocketHandler.send(`connect/${taskId}`); - return taskStates.value[taskId]; + return taskStates[taskId]; }; diff --git a/deploy-template/compose.yml b/deploy-template/compose.yml index ba696e7..d3011a6 100644 --- a/deploy-template/compose.yml +++ b/deploy-template/compose.yml @@ -4,7 +4,7 @@ services: ports: - 5432:5432 healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "postgres"] + test: pg_isready -d drop -U drop interval: 30s timeout: 60s retries: 5 diff --git a/drop-base b/drop-base new file mode 160000 index 0000000..01fd41c --- /dev/null +++ b/drop-base @@ -0,0 +1 @@ +Subproject commit 01fd41c65ae288eb19bbc92b2625733afe51c101 diff --git a/layouts/admin.vue b/layouts/admin.vue index aae898c..52ca79f 100644 --- a/layouts/admin.vue +++ b/layouts/admin.vue @@ -1,33 +1,72 @@ + + + + + + + + + + + + + Close sidebar + + + + + + + + + + + + + + + {{ item.label }} + + + + + + + + + + + + class="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:z-50 lg:block lg:w-20 lg:overflow-y-auto lg:bg-zinc-950 lg:pb-4"> - Admin + Admin - - + + {{ item.label }} @@ -35,14 +74,8 @@ - - + + Open sidebar @@ -59,6 +92,16 @@ diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue index 794c0ca..954dd28 100644 --- a/pages/admin/library/import.vue +++ b/pages/admin/library/import.vue @@ -1,70 +1,39 @@ - - - Select game to import + + + Select game to import + class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"> {{ games.unimportedGames[currentlySelectedGame] }} - Please select a directory... - - + Please select a directory... + + - + - - - {{ game }} + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"> + + + {{ game }} - + @@ -74,106 +43,97 @@ - - Select game - - - - Please select a game... - - - - + + + + importGame_wrapper(false)" class="w-fit" :loading="importLoading">Import without + metadata + - - - - - - - - - - - - Loading game results... - - - - - Loading... - - - importGame_wrapper()" - class="w-fit" - :loading="importLoading" - >Import + + + OR + + - - - - + + + + Select game + + + + Please select a game... + + + + + + + + + + + + + + - - - {{ importError }} - + + + Loading game results... + + + + + Loading... + + + + + + + + + + {{ gameSearchResultsError }} + + + + + + + importGame_wrapper()" class="w-fit" :loading="importLoading" + :disabled="currentlySelectedMetadata === -1">Import + + + + + + + + + + {{ importError }} + + + @@ -201,6 +161,8 @@ const headers = useRequestHeaders(["cookie"]); const games = await $fetch("/api/v1/admin/import/game", { headers }); const currentlySelectedGame = ref(-1); +const gameSearchResultsLoading = ref(false); +const gameSearchResultsError = ref(); async function updateSelectedGame(value: number) { if (currentlySelectedGame.value == value) return; @@ -218,6 +180,15 @@ async function updateSelectedGame(value: number) { metadataResults.value = results; } +function updateSelectedGame_wrapper(value: number) { + gameSearchResultsLoading.value = true; + updateSelectedGame(value).catch((error) => { + gameSearchResultsError.value = error.statusMessage || "An unknown error occurred"; + }).finally(() => { + gameSearchResultsLoading.value = false; + }) +} + const metadataResults = ref | undefined>(); const currentlySelectedMetadata = ref(-1); @@ -225,23 +196,23 @@ const router = useRouter(); const importLoading = ref(false); const importError = ref(); -async function importGame() { +async function importGame(metadata: boolean) { if (!metadataResults.value) return; const game = await $fetch("/api/v1/admin/import/game", { method: "POST", body: { path: games.unimportedGames[currentlySelectedGame.value], - metadata: metadataResults.value[currentlySelectedMetadata.value], + metadata: metadata ? metadataResults.value[currentlySelectedMetadata.value] : undefined, }, }); router.push(`/admin/library/${game.id}`); } -function importGame_wrapper() { +function importGame_wrapper(metadata = true) { importLoading.value = true; importError.value = undefined; - importGame() + importGame(metadata) .catch((error) => { importError.value = error?.statusMessage || "An unknown error occurred."; }) diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index 4545bbe..e50b9eb 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -66,7 +66,7 @@ > diff --git a/pages/admin/task/[id]/index.vue b/pages/admin/task/[id]/index.vue index 36833a8..fee6e7f 100644 --- a/pages/admin/task/[id]/index.vue +++ b/pages/admin/task/[id]/index.vue @@ -1,7 +1,7 @@ @@ -11,37 +11,78 @@ - "{{ taskValue.name }}" completed successfully. + "{{ task.name }}" completed successfully. - + + + + + + {{ task.error.title }} + + + + {{ task.error.description }} + + + + + + - {{ taskValue.name }} + {{ task.name }} - {{ line }} + {{ line }} + + + + + + Loading... + +
- "{{ taskValue.name }}" completed successfully. + "{{ task.name }}" completed successfully.
+ {{ task.error.description }} +
{{ line }}