You've already forked pages
forked from didirus/AstralRinth
fix: disable start button on backup restore/create (#4582)
* fix: CLAUDE.md * fix: allowing start server on backup create/restore --------- Signed-off-by: Calum H. <contact@cal.engineer>
This commit is contained in:
31
CLAUDE.md
31
CLAUDE.md
@@ -1,5 +1,35 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
|
Use TAB instead of spaces.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).
|
||||||
|
|
||||||
|
Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
|
||||||
|
|
||||||
|
Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.
|
||||||
|
|
||||||
|
Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.
|
||||||
|
|
||||||
|
Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.
|
||||||
|
|
||||||
|
### Website (apps/frontend)
|
||||||
|
|
||||||
|
Before a pull request can be opened for the website, `pnpm web:fix` and `pnpm web:intl:extract` must be run, otherwise CI will fail.
|
||||||
|
|
||||||
|
To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.
|
||||||
|
|
||||||
|
### App Frontend (apps/app-frontend)
|
||||||
|
|
||||||
|
Before a pull request can be opened for the website, you must CD into the `app-frontend` folder; `pnpm fix` and `pnpm intl:extract` must be run, otherwise CI will fail.
|
||||||
|
|
||||||
|
To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.
|
||||||
|
|
||||||
## Labrinth
|
## Labrinth
|
||||||
|
|
||||||
Labrinth is the backend API service for Modrinth.
|
Labrinth is the backend API service for Modrinth.
|
||||||
@@ -15,6 +45,7 @@ To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`.
|
|||||||
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
|
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
|
||||||
|
|
||||||
When the user refers to "performing pre-PR checks", do the following:
|
When the user refers to "performing pre-PR checks", do the following:
|
||||||
|
|
||||||
- Run clippy as described above
|
- Run clippy as described above
|
||||||
- DO NOT run tests unless explicitly requested (they take a long time)
|
- DO NOT run tests unless explicitly requested (they take a long time)
|
||||||
- Prepare the sqlx cache
|
- Prepare the sqlx cache
|
||||||
|
|||||||
@@ -68,7 +68,11 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<ButtonStyled type="standard" color="brand">
|
<ButtonStyled type="standard" color="brand">
|
||||||
<button :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button
|
||||||
|
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||||
|
:disabled="!canTakeAction"
|
||||||
|
@click="handlePrimaryAction"
|
||||||
|
>
|
||||||
<div v-if="isTransitionState" class="grid place-content-center">
|
<div v-if="isTransitionState" class="grid place-content-center">
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
</div>
|
</div>
|
||||||
@@ -122,12 +126,15 @@ import { useStorage } from '@vueuse/core'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||||
|
|
||||||
import LoadingIcon from './icons/LoadingIcon.vue'
|
import LoadingIcon from './icons/LoadingIcon.vue'
|
||||||
import PanelSpinner from './PanelSpinner.vue'
|
import PanelSpinner from './PanelSpinner.vue'
|
||||||
import ServerInfoLabels from './ServerInfoLabels.vue'
|
import ServerInfoLabels from './ServerInfoLabels.vue'
|
||||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||||
|
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
interface PowerAction {
|
interface PowerAction {
|
||||||
action: ServerPowerAction
|
action: ServerPowerAction
|
||||||
@@ -142,6 +149,7 @@ const props = defineProps<{
|
|||||||
serverName?: string
|
serverName?: string
|
||||||
serverData: object
|
serverData: object
|
||||||
uptimeSeconds: number
|
uptimeSeconds: number
|
||||||
|
backupInProgress?: BackupInProgressReason
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -163,7 +171,11 @@ const dontAskAgain = ref(false)
|
|||||||
const startingDelay = ref(false)
|
const startingDelay = ref(false)
|
||||||
|
|
||||||
const canTakeAction = computed(
|
const canTakeAction = computed(
|
||||||
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
|
() =>
|
||||||
|
!props.isActioning &&
|
||||||
|
!startingDelay.value &&
|
||||||
|
!isTransitionState.value &&
|
||||||
|
!props.backupInProgress,
|
||||||
)
|
)
|
||||||
const isRunning = computed(() => serverState.value === 'running')
|
const isRunning = computed(() => serverState.value === 'running')
|
||||||
const isTransitionState = computed(() =>
|
const isTransitionState = computed(() =>
|
||||||
|
|||||||
@@ -11,12 +11,35 @@ export class BackupsModule extends ServerModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(backupName: string): Promise<string> {
|
async create(backupName: string): Promise<string> {
|
||||||
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
|
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
||||||
method: 'POST',
|
const tempBackup: Backup = {
|
||||||
body: { name: backupName },
|
id: tempId,
|
||||||
})
|
name: backupName,
|
||||||
await this.fetch() // Refresh this module
|
created_at: new Date().toISOString(),
|
||||||
return response.id
|
locked: false,
|
||||||
|
automated: false,
|
||||||
|
interrupted: false,
|
||||||
|
ongoing: true,
|
||||||
|
task: { create: { progress: 0, state: 'ongoing' } },
|
||||||
|
}
|
||||||
|
this.data.push(tempBackup)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name: backupName },
|
||||||
|
})
|
||||||
|
|
||||||
|
const backup = this.data.find((b) => b.id === tempId)
|
||||||
|
if (backup) {
|
||||||
|
backup.id = response.id
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.id
|
||||||
|
} catch (error) {
|
||||||
|
this.data = this.data.filter((b) => b.id !== tempId)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async rename(backupId: string, newName: string): Promise<void> {
|
async rename(backupId: string, newName: string): Promise<void> {
|
||||||
@@ -24,35 +47,47 @@ export class BackupsModule extends ServerModule {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name: newName },
|
body: { name: newName },
|
||||||
})
|
})
|
||||||
await this.fetch() // Refresh this module
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(backupId: string): Promise<void> {
|
async delete(backupId: string): Promise<void> {
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
|
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
await this.fetch() // Refresh this module
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(backupId: string): Promise<void> {
|
async restore(backupId: string): Promise<void> {
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
|
const backup = this.data.find((b) => b.id === backupId)
|
||||||
method: 'POST',
|
if (backup) {
|
||||||
})
|
if (!backup.task) backup.task = {}
|
||||||
await this.fetch() // Refresh this module
|
backup.task.restore = { progress: 0, state: 'ongoing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (backup?.task?.restore) {
|
||||||
|
delete backup.task.restore
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async lock(backupId: string): Promise<void> {
|
async lock(backupId: string): Promise<void> {
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
})
|
})
|
||||||
await this.fetch() // Refresh this module
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlock(backupId: string): Promise<void> {
|
async unlock(backupId: string): Promise<void> {
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
|
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
})
|
})
|
||||||
await this.fetch() // Refresh this module
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async retry(backupId: string): Promise<void> {
|
async retry(backupId: string): Promise<void> {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@
|
|||||||
:server-name="serverData.name"
|
:server-name="serverData.name"
|
||||||
:server-data="serverData"
|
:server-data="serverData"
|
||||||
:uptime-seconds="uptimeSeconds"
|
:uptime-seconds="uptimeSeconds"
|
||||||
|
:backup-in-progress="backupInProgress"
|
||||||
@action="sendPowerAction"
|
@action="sendPowerAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,7 +355,7 @@
|
|||||||
>
|
>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
||||||
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
||||||
JSON.stringify(server, null, ' ')
|
JSON.stringify(server, null, ' ')
|
||||||
}}</pre>
|
}}</pre>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -759,9 +760,14 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
|||||||
curBackup.task = {}
|
curBackup.task = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
curBackup.task[data.task] = {
|
const currentState = curBackup.task[data.task]?.state
|
||||||
progress: data.progress,
|
const shouldUpdate = !(currentState === 'ongoing' && data.state === 'unchanged')
|
||||||
state: data.state,
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
curBackup.task[data.task] = {
|
||||||
|
progress: data.progress,
|
||||||
|
state: data.state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
curBackup.ongoing = data.task === 'create' && data.state === 'ongoing'
|
curBackup.ongoing = data.task === 'create' && data.state === 'ongoing'
|
||||||
@@ -1277,6 +1283,7 @@ useHead({
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(1rem);
|
transform: translateX(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user