mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
fix: qa + lint + i18n
This commit is contained in:
@@ -211,7 +211,9 @@ export function useServerInstallContent({
|
|||||||
const { data: serverContentData, error: serverContentError } = useQuery({
|
const { data: serverContentData, error: serverContentError } = useQuery({
|
||||||
queryKey: contentQueryKey,
|
queryKey: contentQueryKey,
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
client.archon.content_v1.getAddons(currentServerId.value!, currentWorldId.value!),
|
client.archon.content_v1.getAddons(currentServerId.value!, currentWorldId.value!, {
|
||||||
|
from_modpack: false,
|
||||||
|
}),
|
||||||
enabled: computed(() => !!currentServerId.value && !!currentWorldId.value),
|
enabled: computed(() => !!currentServerId.value && !!currentWorldId.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type { TabbedModalTab } from '#ui/components'
|
|||||||
import { TabbedModal } from '#ui/components'
|
import { TabbedModal } from '#ui/components'
|
||||||
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import {
|
import {
|
||||||
ServerSettingsAdvancedPage,
|
|
||||||
ServerSettingsGeneralPage,
|
ServerSettingsGeneralPage,
|
||||||
ServerSettingsNetworkPage,
|
ServerSettingsNetworkPage,
|
||||||
serverSettingsTabDefinitions,
|
serverSettingsTabDefinitions,
|
||||||
@@ -71,7 +70,6 @@ useQuery({
|
|||||||
const serverSettingsTabComponentMap = {
|
const serverSettingsTabComponentMap = {
|
||||||
general: ServerSettingsGeneralPage,
|
general: ServerSettingsGeneralPage,
|
||||||
network: ServerSettingsNetworkPage,
|
network: ServerSettingsNetworkPage,
|
||||||
advanced: ServerSettingsAdvancedPage,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
provideServerSettings({
|
provideServerSettings({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import Admonition from '#ui/components/base/Admonition.vue'
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
@@ -45,6 +45,15 @@ const messages = defineMessages({
|
|||||||
id: 'servers.admonitions.background-task-running',
|
id: 'servers.admonitions.background-task-running',
|
||||||
defaultMessage: 'Background task running',
|
defaultMessage: 'Background task running',
|
||||||
},
|
},
|
||||||
|
instanceInfoHeader: {
|
||||||
|
id: 'servers.manage.instances.info.header',
|
||||||
|
defaultMessage: 'What is a server instance?',
|
||||||
|
},
|
||||||
|
instanceInfoBody: {
|
||||||
|
id: 'servers.manage.instances.info.body',
|
||||||
|
defaultMessage:
|
||||||
|
'An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time.',
|
||||||
|
},
|
||||||
contentBusyBody: {
|
contentBusyBody: {
|
||||||
id: 'content.page-layout.busy-description',
|
id: 'content.page-layout.busy-description',
|
||||||
defaultMessage: 'Please wait for the operation to complete before editing content.',
|
defaultMessage: 'Please wait for the operation to complete before editing content.',
|
||||||
@@ -56,6 +65,9 @@ const messages = defineMessages({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
||||||
|
const isOnInstancesList = computed(
|
||||||
|
() => route.path.includes('/instances') && !route.params.instance_id,
|
||||||
|
)
|
||||||
const isOnContentTab = computed(
|
const isOnContentTab = computed(
|
||||||
() =>
|
() =>
|
||||||
route.path.includes('/content') ||
|
route.path.includes('/content') ||
|
||||||
@@ -100,6 +112,28 @@ const dismissedIds = reactive(new Set<string>())
|
|||||||
const cancellingIds = reactive(new Set<string>())
|
const cancellingIds = reactive(new Set<string>())
|
||||||
const uploadCancelling = ref(false)
|
const uploadCancelling = ref(false)
|
||||||
const dismissedContentErrorKey = ref<string | null>(null)
|
const dismissedContentErrorKey = ref<string | null>(null)
|
||||||
|
const instanceInfoAdmonitionStorageLoaded = ref(false)
|
||||||
|
const instanceInfoAdmonitionDismissed = ref(true)
|
||||||
|
const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed'
|
||||||
|
|
||||||
|
const instanceCount = computed(() => ctx.serverFull.value?.worlds.length ?? null)
|
||||||
|
const showInstanceInfoAdmonition = computed(
|
||||||
|
() =>
|
||||||
|
isOnInstancesList.value &&
|
||||||
|
instanceInfoAdmonitionStorageLoaded.value &&
|
||||||
|
!instanceInfoAdmonitionDismissed.value &&
|
||||||
|
instanceCount.value === 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
instanceInfoAdmonitionDismissed.value =
|
||||||
|
window.localStorage.getItem(INSTANCE_INFO_ADMONITION_KEY) === 'true'
|
||||||
|
instanceInfoAdmonitionStorageLoaded.value = true
|
||||||
|
} catch {
|
||||||
|
instanceInfoAdmonitionStorageLoaded.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const contentErrorKey = computed(() =>
|
const contentErrorKey = computed(() =>
|
||||||
props.contentError ? `${props.contentError.step}:${props.contentError.description}` : null,
|
props.contentError ? `${props.contentError.step}:${props.contentError.description}` : null,
|
||||||
@@ -170,6 +204,7 @@ type ServerAdmonitionItem = StackedAdmonitionItem & {
|
|||||||
| { kind: 'upload' }
|
| { kind: 'upload' }
|
||||||
| { kind: 'fs-op'; op: FileOperation }
|
| { kind: 'fs-op'; op: FileOperation }
|
||||||
| { kind: 'backup'; entry: BackupAdmonitionEntry }
|
| { kind: 'backup'; entry: BackupAdmonitionEntry }
|
||||||
|
| { kind: 'instance-info' }
|
||||||
| { kind: 'busy-content' }
|
| { kind: 'busy-content' }
|
||||||
| { kind: 'busy-files' }
|
| { kind: 'busy-files' }
|
||||||
)
|
)
|
||||||
@@ -259,6 +294,17 @@ const stackItems = computed<ServerAdmonitionItem[]>(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showInstanceInfoAdmonition.value) {
|
||||||
|
out.push({
|
||||||
|
id: 'instance-info',
|
||||||
|
type: 'info',
|
||||||
|
dismissible: true,
|
||||||
|
kind: 'instance-info',
|
||||||
|
priority: 6,
|
||||||
|
sortIndex: sortIndex++,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (contentBusyHeader.value) {
|
if (contentBusyHeader.value) {
|
||||||
const p = isOnContentTab.value ? 0 : 5
|
const p = isOnContentTab.value ? 0 : 5
|
||||||
out.push({
|
out.push({
|
||||||
@@ -378,6 +424,8 @@ async function onDismissAll() {
|
|||||||
}
|
}
|
||||||
} else if (it.kind === 'backup') {
|
} else if (it.kind === 'backup') {
|
||||||
tasks.push(onBackupDismiss(it.entry))
|
tasks.push(onBackupDismiss(it.entry))
|
||||||
|
} else if (it.kind === 'instance-info') {
|
||||||
|
onInstanceInfoDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(tasks)
|
await Promise.all(tasks)
|
||||||
@@ -394,6 +442,15 @@ function onContentErrorDismiss() {
|
|||||||
dismissedContentErrorKey.value = contentErrorKey.value
|
dismissedContentErrorKey.value = contentErrorKey.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInstanceInfoDismiss() {
|
||||||
|
instanceInfoAdmonitionDismissed.value = true
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(INSTANCE_INFO_ADMONITION_KEY, 'true')
|
||||||
|
} catch {
|
||||||
|
instanceInfoAdmonitionStorageLoaded.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -445,6 +502,15 @@ function onContentErrorDismiss() {
|
|||||||
>
|
>
|
||||||
{{ formatMessage(messages.contentBusyBody) }}
|
{{ formatMessage(messages.contentBusyBody) }}
|
||||||
</Admonition>
|
</Admonition>
|
||||||
|
<Admonition
|
||||||
|
v-else-if="item.kind === 'instance-info'"
|
||||||
|
type="info"
|
||||||
|
:header="formatMessage(messages.instanceInfoHeader)"
|
||||||
|
:dismissible="dismissible"
|
||||||
|
@dismiss="onInstanceInfoDismiss"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.instanceInfoBody) }}
|
||||||
|
</Admonition>
|
||||||
<Admonition
|
<Admonition
|
||||||
v-else-if="item.kind === 'busy-files'"
|
v-else-if="item.kind === 'busy-files'"
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|||||||
@@ -296,16 +296,7 @@ const startActionText = computed(() =>
|
|||||||
primaryActionText.value === 'Start' ? 'Start server' : primaryActionText.value,
|
primaryActionText.value === 'Start' ? 'Start server' : primaryActionText.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
const startableWorlds = computed(() => {
|
const powerActionWorlds = computed(() => (props.worlds.length > 1 ? props.worlds : []))
|
||||||
if (props.worlds.length > 0) return props.worlds
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'current-world',
|
|
||||||
name: props.activeWorldName ?? 'current instance',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const startSplitActions = computed<JoinedButtonAction[]>(() => [
|
const startSplitActions = computed<JoinedButtonAction[]>(() => [
|
||||||
{
|
{
|
||||||
@@ -314,7 +305,7 @@ const startSplitActions = computed<JoinedButtonAction[]>(() => [
|
|||||||
icon: PlayIcon,
|
icon: PlayIcon,
|
||||||
action: handlePrimaryAction,
|
action: handlePrimaryAction,
|
||||||
},
|
},
|
||||||
...startableWorlds.value.map((world) => ({
|
...powerActionWorlds.value.map((world) => ({
|
||||||
id: `start-${world.id}`,
|
id: `start-${world.id}`,
|
||||||
label: `Start with ${world.name}`,
|
label: `Start with ${world.name}`,
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
@@ -330,7 +321,7 @@ const restartSplitActions = computed<JoinedButtonAction[]>(() => [
|
|||||||
action: () => initiateAction('Restart'),
|
action: () => initiateAction('Restart'),
|
||||||
},
|
},
|
||||||
// TODO: Implement world scoping when Archon/Kyros support target worlds in power requests.
|
// TODO: Implement world scoping when Archon/Kyros support target worlds in power requests.
|
||||||
...startableWorlds.value.map((world) => ({
|
...powerActionWorlds.value.map((world) => ({
|
||||||
id: `restart-${world.id}`,
|
id: `restart-${world.id}`,
|
||||||
label: `Restart with ${world.name}`,
|
label: `Restart with ${world.name}`,
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
Settings2Icon,
|
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
|
WrenchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Tooltip } from 'floating-vue'
|
import { Tooltip } from 'floating-vue'
|
||||||
@@ -275,7 +275,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Settings2Icon />
|
<WrenchIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +312,7 @@ onUnmounted(() => {
|
|||||||
{{ formatMessage(commonMessages.contentLabel) }}
|
{{ formatMessage(commonMessages.contentLabel) }}
|
||||||
</template>
|
</template>
|
||||||
<template #settings>
|
<template #settings>
|
||||||
<Settings2Icon class="size-5" />
|
<WrenchIcon class="size-5" />
|
||||||
{{ formatMessage(commonMessages.settingsLabel) }}
|
{{ formatMessage(commonMessages.settingsLabel) }}
|
||||||
</template>
|
</template>
|
||||||
</TeleportOverflowMenu></ButtonStyled
|
</TeleportOverflowMenu></ButtonStyled
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative h-full w-full">
|
|
||||||
<div class="flex h-full w-full flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex flex-col items-center justify-between gap-0.5 sm:flex-row">
|
|
||||||
<span class="text-lg font-semibold text-contrast">SFTP</span>
|
|
||||||
<ButtonStyled>
|
|
||||||
<a
|
|
||||||
v-tooltip="sftpActionTooltip"
|
|
||||||
class="!w-full sm:!w-auto"
|
|
||||||
:class="{ 'opacity-60': !canWriteFiles }"
|
|
||||||
:href="canWriteFiles ? sftpUrl : undefined"
|
|
||||||
:aria-disabled="!canWriteFiles"
|
|
||||||
target="_blank"
|
|
||||||
@click="handleSftpLaunchClick"
|
|
||||||
>
|
|
||||||
<ExternalIcon class="h-5 w-5" />
|
|
||||||
Launch SFTP
|
|
||||||
</a>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
|
|
||||||
<span class="text-lg font-semibold text-contrast">Server Address</span>
|
|
||||||
<div
|
|
||||||
v-tooltip="sftpCopyTooltip('Copy SFTP server address')"
|
|
||||||
class="copy-field hover:bg-button-bg-hover"
|
|
||||||
:class="{ 'opacity-60': !canWriteFiles }"
|
|
||||||
@click="copyToClipboard('Server address', server?.sftp_host)"
|
|
||||||
>
|
|
||||||
<span class="cursor-pointer font-semibold text-primary">
|
|
||||||
{{ server?.sftp_host }}
|
|
||||||
</span>
|
|
||||||
<div class="grid h-10 w-10 place-content-center">
|
|
||||||
<CopyIcon class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
|
||||||
<div class="flex w-full flex-col justify-center gap-2">
|
|
||||||
<span class="text-lg font-semibold text-contrast">Username</span>
|
|
||||||
<div
|
|
||||||
v-tooltip="sftpCopyTooltip('Copy SFTP username')"
|
|
||||||
class="copy-field hover:bg-button-bg-hover"
|
|
||||||
:class="{ 'opacity-60': !canWriteFiles }"
|
|
||||||
@click="copyToClipboard('Username', server?.sftp_username)"
|
|
||||||
>
|
|
||||||
<div class="truncate font-semibold">
|
|
||||||
{{ server?.sftp_username }}
|
|
||||||
</div>
|
|
||||||
<div class="grid h-10 w-9 place-content-center">
|
|
||||||
<CopyIcon class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full flex-col justify-center gap-2">
|
|
||||||
<span class="text-lg font-semibold text-contrast">Password</span>
|
|
||||||
<div
|
|
||||||
class="copy-field-has-button [&:hover:not(:has(button:hover))]:bg-button-bg-hover"
|
|
||||||
:class="{ 'opacity-60': !canWriteFiles }"
|
|
||||||
@click="copyToClipboard('Password', server?.sftp_password)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-1.5 h-full w-full">
|
|
||||||
<div
|
|
||||||
v-tooltip="sftpCopyTooltip('Copy SFTP Password')"
|
|
||||||
class="h-full flex justify-between grow items-center"
|
|
||||||
>
|
|
||||||
<div class="truncate font-semibold">
|
|
||||||
{{
|
|
||||||
showPassword
|
|
||||||
? server?.sftp_password
|
|
||||||
: '*'.repeat(server?.sftp_password?.length ?? 0)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<CopyIcon class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ButtonStyled type="transparent" circular>
|
|
||||||
<button
|
|
||||||
v-tooltip="
|
|
||||||
canWriteFiles
|
|
||||||
? showPassword
|
|
||||||
? 'Hide password'
|
|
||||||
: 'Show password'
|
|
||||||
: permissionDeniedMessage
|
|
||||||
"
|
|
||||||
class="hover:bg-button-bg-hover grid h-10 w-10 place-content-center rounded-lg"
|
|
||||||
:disabled="!canWriteFiles"
|
|
||||||
@click.stop="togglePasswordVisibility"
|
|
||||||
>
|
|
||||||
<EyeIcon v-if="showPassword" class="h-5 w-5" />
|
|
||||||
<EyeOffIcon v-else class="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from '@modrinth/assets'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
import { ButtonStyled } from '#ui/components'
|
|
||||||
import { useServerPermissions } from '#ui/composables/server-permissions'
|
|
||||||
import { injectModrinthServerContext, injectNotificationManager } from '#ui/providers'
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
|
||||||
const { server } = injectModrinthServerContext()
|
|
||||||
const { canWriteFiles, permissionDeniedMessage } = useServerPermissions()
|
|
||||||
|
|
||||||
const showPassword = ref(false)
|
|
||||||
const sftpUrl = computed(() => `sftp://${server.value?.sftp_username}@${server.value?.sftp_host}`)
|
|
||||||
const sftpActionTooltip = computed(() =>
|
|
||||||
canWriteFiles.value
|
|
||||||
? 'This button only works with compatible SFTP clients (e.g. WinSCP)'
|
|
||||||
: permissionDeniedMessage.value,
|
|
||||||
)
|
|
||||||
const sftpCopyTooltip = (label: string) =>
|
|
||||||
canWriteFiles.value ? label : permissionDeniedMessage.value
|
|
||||||
|
|
||||||
function handleSftpLaunchClick(event: MouseEvent) {
|
|
||||||
if (canWriteFiles.value) return
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyToClipboard = (name: string, textToCopy?: string) => {
|
|
||||||
if (!canWriteFiles.value) return
|
|
||||||
navigator.clipboard.writeText(textToCopy || '')
|
|
||||||
addNotification({
|
|
||||||
type: 'success',
|
|
||||||
title: `${name} copied to clipboard!`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePasswordVisibility() {
|
|
||||||
if (!canWriteFiles.value) return
|
|
||||||
showPassword.value = !showPassword.value
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.copy-field {
|
|
||||||
@apply flex h-10 cursor-pointer items-center justify-between gap-2 rounded-lg bg-button-bg px-3 pr-1.5 transition-all;
|
|
||||||
@apply hover:brightness-125 active:scale-95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-field-has-button {
|
|
||||||
@apply flex h-10 cursor-pointer items-center justify-between gap-2 rounded-lg bg-button-bg px-3 pr-1.5 transition-all;
|
|
||||||
@apply [&:hover:not(:has(button:hover))]:brightness-125 [&:active:not(:has(button:active))]:scale-95;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as ServerSettingsAdvancedPage } from './advanced.vue'
|
|
||||||
export { default as ServerSettingsGeneralPage } from './general.vue'
|
export { default as ServerSettingsGeneralPage } from './general.vue'
|
||||||
export { default as ServerSettingsInstallationPage } from './installation.vue'
|
export { default as ServerSettingsInstallationPage } from './installation.vue'
|
||||||
export { default as ServerInstanceSettingsAdvancedPage } from './instance-advanced.vue'
|
export { default as ServerInstanceSettingsAdvancedPage } from './instance-advanced.vue'
|
||||||
|
|||||||
@@ -204,6 +204,103 @@
|
|||||||
Set up your personal domain to connect to your server via custom DNS records.
|
Set up your personal domain to connect to your server via custom DNS records.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col items-center justify-between gap-0.5 sm:flex-row">
|
||||||
|
<span class="text-lg font-semibold text-contrast">SFTP</span>
|
||||||
|
<ButtonStyled>
|
||||||
|
<a
|
||||||
|
v-tooltip="sftpActionTooltip"
|
||||||
|
class="!w-full sm:!w-auto"
|
||||||
|
:class="{ 'opacity-60': !canWriteFiles }"
|
||||||
|
:href="canWriteFiles ? sftpUrl : undefined"
|
||||||
|
:aria-disabled="!canWriteFiles"
|
||||||
|
target="_blank"
|
||||||
|
@click="handleSftpLaunchClick"
|
||||||
|
>
|
||||||
|
<ExternalIcon class="h-5 w-5" />
|
||||||
|
Launch SFTP
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
|
||||||
|
<span class="text-lg font-semibold text-contrast">Server Address</span>
|
||||||
|
<div
|
||||||
|
v-tooltip="sftpCopyTooltip('Copy SFTP server address')"
|
||||||
|
class="copy-field hover:bg-button-bg-hover"
|
||||||
|
:class="{ 'opacity-60': !canWriteFiles }"
|
||||||
|
@click="copyToClipboard('Server address', server?.sftp_host)"
|
||||||
|
>
|
||||||
|
<span class="cursor-pointer font-semibold text-primary">
|
||||||
|
{{ server?.sftp_host }}
|
||||||
|
</span>
|
||||||
|
<div class="grid h-10 w-10 place-content-center">
|
||||||
|
<CopyIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
||||||
|
<div class="flex w-full flex-col justify-center gap-2">
|
||||||
|
<span class="text-lg font-semibold text-contrast">Username</span>
|
||||||
|
<div
|
||||||
|
v-tooltip="sftpCopyTooltip('Copy SFTP username')"
|
||||||
|
class="copy-field hover:bg-button-bg-hover"
|
||||||
|
:class="{ 'opacity-60': !canWriteFiles }"
|
||||||
|
@click="copyToClipboard('Username', server?.sftp_username)"
|
||||||
|
>
|
||||||
|
<div class="truncate font-semibold">
|
||||||
|
{{ server?.sftp_username }}
|
||||||
|
</div>
|
||||||
|
<div class="grid h-10 w-9 place-content-center">
|
||||||
|
<CopyIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col justify-center gap-2">
|
||||||
|
<span class="text-lg font-semibold text-contrast">Password</span>
|
||||||
|
<div
|
||||||
|
class="copy-field-has-button [&:hover:not(:has(button:hover))]:bg-button-bg-hover"
|
||||||
|
:class="{ 'opacity-60': !canWriteFiles }"
|
||||||
|
@click="copyToClipboard('Password', server?.sftp_password)"
|
||||||
|
>
|
||||||
|
<div class="flex h-full w-full items-center gap-1.5">
|
||||||
|
<div
|
||||||
|
v-tooltip="sftpCopyTooltip('Copy SFTP Password')"
|
||||||
|
class="flex h-full grow items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="truncate font-semibold">
|
||||||
|
{{
|
||||||
|
showPassword
|
||||||
|
? server?.sftp_password
|
||||||
|
: '*'.repeat(server?.sftp_password?.length ?? 0)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<CopyIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
canWriteFiles
|
||||||
|
? showPassword
|
||||||
|
? 'Hide password'
|
||||||
|
: 'Show password'
|
||||||
|
: permissionDeniedMessage
|
||||||
|
"
|
||||||
|
class="grid h-10 w-10 place-content-center rounded-lg hover:bg-button-bg-hover"
|
||||||
|
:disabled="!canWriteFiles"
|
||||||
|
@click.stop="togglePasswordVisibility"
|
||||||
|
>
|
||||||
|
<EyeIcon v-if="showPassword" class="h-5 w-5" />
|
||||||
|
<EyeOffIcon v-else class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +310,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
EyeIcon,
|
||||||
|
EyeOffIcon,
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -236,7 +336,7 @@ const { addNotification } = injectNotificationManager()
|
|||||||
const { server, serverId } = injectModrinthServerContext()
|
const { server, serverId } = injectModrinthServerContext()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { canUseAdvancedSettings, permissionDeniedMessage } = useServerPermissions()
|
const { canUseAdvancedSettings, canWriteFiles, permissionDeniedMessage } = useServerPermissions()
|
||||||
|
|
||||||
const data = server
|
const data = server
|
||||||
|
|
||||||
@@ -289,6 +389,7 @@ const editAllocationName = ref('')
|
|||||||
const newAllocationPort = ref(0)
|
const newAllocationPort = ref(0)
|
||||||
const allocationToDelete = ref<number | null>(null)
|
const allocationToDelete = ref<number | null>(null)
|
||||||
const creatingAllocation = ref(false)
|
const creatingAllocation = ref(false)
|
||||||
|
const showPassword = ref(false)
|
||||||
const advancedActionTooltip = computed(() =>
|
const advancedActionTooltip = computed(() =>
|
||||||
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
|
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
|
||||||
)
|
)
|
||||||
@@ -297,6 +398,33 @@ const createAllocationTooltip = computed(() => {
|
|||||||
if (!createAllocationName.value) return 'Enter a name to create an allocation'
|
if (!createAllocationName.value) return 'Enter a name to create an allocation'
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
const sftpUrl = computed(() => `sftp://${server.value?.sftp_username}@${server.value?.sftp_host}`)
|
||||||
|
const sftpActionTooltip = computed(() =>
|
||||||
|
canWriteFiles.value
|
||||||
|
? 'This button only works with compatible SFTP clients (e.g. WinSCP)'
|
||||||
|
: permissionDeniedMessage.value,
|
||||||
|
)
|
||||||
|
const sftpCopyTooltip = (label: string) =>
|
||||||
|
canWriteFiles.value ? label : permissionDeniedMessage.value
|
||||||
|
|
||||||
|
function handleSftpLaunchClick(event: MouseEvent) {
|
||||||
|
if (canWriteFiles.value) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(name: string, textToCopy?: string) {
|
||||||
|
if (!canWriteFiles.value) return
|
||||||
|
navigator.clipboard.writeText(textToCopy || '')
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: `${name} copied to clipboard!`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePasswordVisibility() {
|
||||||
|
if (!canWriteFiles.value) return
|
||||||
|
showPassword.value = !showPassword.value
|
||||||
|
}
|
||||||
|
|
||||||
const addNewAllocation = async () => {
|
const addNewAllocation = async () => {
|
||||||
if (!canUseAdvancedSettings.value) return
|
if (!canUseAdvancedSettings.value) return
|
||||||
@@ -440,3 +568,15 @@ const copyText = (text: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.copy-field {
|
||||||
|
@apply flex h-10 cursor-pointer items-center justify-between gap-2 rounded-lg bg-button-bg px-3 pr-1.5 transition-all;
|
||||||
|
@apply hover:brightness-125 active:scale-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-field-has-button {
|
||||||
|
@apply flex h-10 cursor-pointer items-center justify-between gap-2 rounded-lg bg-button-bg px-3 pr-1.5 transition-all;
|
||||||
|
@apply [&:hover:not(:has(button:hover))]:brightness-125 [&:active:not(:has(button:active))]:scale-95;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
export type ServerSettingsTabId = 'general' | 'network' | 'advanced' | 'billing' | 'admin-billing'
|
export type ServerSettingsTabId = 'general' | 'network' | 'billing' | 'admin-billing'
|
||||||
|
|
||||||
export type ServerInstanceSettingsTabId = 'general' | 'installation' | 'properties' | 'advanced'
|
export type ServerInstanceSettingsTabId = 'general' | 'installation' | 'properties' | 'advanced'
|
||||||
|
|
||||||
@@ -51,11 +51,6 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
|
|||||||
label: 'Network',
|
label: 'Network',
|
||||||
icon: VersionIcon,
|
icon: VersionIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'advanced',
|
|
||||||
label: 'Advanced',
|
|
||||||
icon: TextQuoteIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'billing',
|
id: 'billing',
|
||||||
label: 'Billing',
|
label: 'Billing',
|
||||||
|
|||||||
@@ -1,30 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div
|
|
||||||
v-if="!instanceInfoAdmonitionDismissed"
|
|
||||||
class="grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-start gap-x-3 rounded-2xl border border-solid border-brand-blue bg-bg-blue p-5 pr-4 text-contrast"
|
|
||||||
>
|
|
||||||
<InfoIcon class="mt-0.5 size-6 text-brand-blue" aria-hidden="true" />
|
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
|
||||||
<h2 class="m-0 text-xl font-bold leading-7">
|
|
||||||
{{ formatMessage(messages.instanceInfoHeader) }}
|
|
||||||
</h2>
|
|
||||||
<p class="m-0 text-lg leading-7 text-contrast/85">
|
|
||||||
{{ formatMessage(messages.instanceInfoBody) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ButtonStyled circular type="transparent" color="blue" hover-color-fill="background">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="mt-0.5"
|
|
||||||
:aria-label="formatMessage(messages.instanceInfoDismiss)"
|
|
||||||
@click="dismissInstanceInfoAdmonition"
|
|
||||||
>
|
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="worldsPending"
|
v-if="worldsPending"
|
||||||
class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,20.25rem),1fr))] gap-6"
|
class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,20.25rem),1fr))] gap-6"
|
||||||
@@ -65,9 +40,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { InfoIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useStorage } from '@vueuse/core'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -76,7 +49,6 @@ import {
|
|||||||
CreationFlowModal,
|
CreationFlowModal,
|
||||||
UploadProgressModal,
|
UploadProgressModal,
|
||||||
} from '#ui/components'
|
} from '#ui/components'
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
|
||||||
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import { useServerPermissions } from '#ui/composables/server-permissions'
|
import { useServerPermissions } from '#ui/composables/server-permissions'
|
||||||
@@ -93,19 +65,6 @@ const messages = defineMessages({
|
|||||||
id: 'servers.manage.instances.slot-name',
|
id: 'servers.manage.instances.slot-name',
|
||||||
defaultMessage: 'Instance #{index}',
|
defaultMessage: 'Instance #{index}',
|
||||||
},
|
},
|
||||||
instanceInfoHeader: {
|
|
||||||
id: 'servers.manage.instances.info.header',
|
|
||||||
defaultMessage: 'What is a server instance?',
|
|
||||||
},
|
|
||||||
instanceInfoBody: {
|
|
||||||
id: 'servers.manage.instances.info.body',
|
|
||||||
defaultMessage:
|
|
||||||
'An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time.',
|
|
||||||
},
|
|
||||||
instanceInfoDismiss: {
|
|
||||||
id: 'servers.manage.instances.info.dismiss',
|
|
||||||
defaultMessage: "Don't show this again",
|
|
||||||
},
|
|
||||||
createSuccessTitle: {
|
createSuccessTitle: {
|
||||||
id: 'servers.manage.instances.create.success.title',
|
id: 'servers.manage.instances.create.success.title',
|
||||||
defaultMessage: 'Instance created',
|
defaultMessage: 'Instance created',
|
||||||
@@ -154,7 +113,6 @@ type ContentSummary = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WORLD_SLOT_COUNT = 3
|
const WORLD_SLOT_COUNT = 3
|
||||||
const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed'
|
|
||||||
const SERVER_LOADERS = ['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']
|
const SERVER_LOADERS = ['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
@@ -166,7 +124,6 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { canSetup, permissionDeniedMessage } = useServerPermissions()
|
const { canSetup, permissionDeniedMessage } = useServerPermissions()
|
||||||
const instanceInfoAdmonitionDismissed = useStorage(INSTANCE_INFO_ADMONITION_KEY, false)
|
|
||||||
const createWorldModalRef =
|
const createWorldModalRef =
|
||||||
useTemplateRef<InstanceType<typeof CreationFlowModal>>('createWorldModalRef')
|
useTemplateRef<InstanceType<typeof CreationFlowModal>>('createWorldModalRef')
|
||||||
const uploadProgressModal =
|
const uploadProgressModal =
|
||||||
@@ -405,10 +362,6 @@ function handleCreateWorld() {
|
|||||||
createWorldModalRef.value?.show()
|
createWorldModalRef.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissInstanceInfoAdmonition() {
|
|
||||||
instanceInfoAdmonitionDismissed.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBrowseModpacks() {
|
function handleBrowseModpacks() {
|
||||||
if (!canSetup.value) return
|
if (!canSetup.value) return
|
||||||
|
|
||||||
|
|||||||
@@ -4766,9 +4766,6 @@
|
|||||||
"servers.manage.instances.info.body": {
|
"servers.manage.instances.info.body": {
|
||||||
"defaultMessage": "An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time."
|
"defaultMessage": "An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time."
|
||||||
},
|
},
|
||||||
"servers.manage.instances.info.dismiss": {
|
|
||||||
"defaultMessage": "Don't show this again"
|
|
||||||
},
|
|
||||||
"servers.manage.instances.info.header": {
|
"servers.manage.instances.info.header": {
|
||||||
"defaultMessage": "What is a server instance?"
|
"defaultMessage": "What is a server instance?"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import type {
|
|||||||
ServerStats,
|
ServerStats,
|
||||||
} from '../../providers/server-context'
|
} from '../../providers/server-context'
|
||||||
|
|
||||||
|
const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Servers/ServerPanelAdmonitions',
|
title: 'Servers/ServerPanelAdmonitions',
|
||||||
component: ServerPanelAdmonitions,
|
component: ServerPanelAdmonitions,
|
||||||
@@ -20,13 +22,25 @@ const meta = {
|
|||||||
layout: 'padded',
|
layout: 'padded',
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(story) => ({
|
(story, context) => ({
|
||||||
components: { story },
|
components: { story },
|
||||||
setup() {
|
setup() {
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
context.parameters.resetInstanceInfoAdmonition === true
|
||||||
|
) {
|
||||||
|
window.localStorage.removeItem(INSTANCE_INFO_ADMONITION_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
router.replace('/hosting/manage/demo-server/instances/demo-world')
|
router.replace(
|
||||||
|
(context.parameters.routePath as string | undefined) ??
|
||||||
|
'/hosting/manage/demo-server/instances/demo-world',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
const serverWorlds =
|
||||||
|
(context.parameters.serverWorlds as Archon.Servers.v1.WorldFull[] | undefined) ?? null
|
||||||
|
|
||||||
const server = ref({
|
const server = ref({
|
||||||
server_id: 'demo-server',
|
server_id: 'demo-server',
|
||||||
@@ -79,7 +93,9 @@ const meta = {
|
|||||||
},
|
},
|
||||||
worldId: ref(null),
|
worldId: ref(null),
|
||||||
server,
|
server,
|
||||||
serverFull: computed(() => null),
|
serverFull: computed(() =>
|
||||||
|
serverWorlds ? ({ worlds: serverWorlds } as Archon.Servers.v1.ServerFull) : null,
|
||||||
|
),
|
||||||
currentUserPermissions: computed(() => 0),
|
currentUserPermissions: computed(() => 0),
|
||||||
isConnected: ref(true),
|
isConnected: ref(true),
|
||||||
isWsAuthIncorrect: ref(false),
|
isWsAuthIncorrect: ref(false),
|
||||||
@@ -120,3 +136,11 @@ export default meta
|
|||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const WithUploadFileOpAndBusy: Story = {}
|
export const WithUploadFileOpAndBusy: Story = {}
|
||||||
|
|
||||||
|
export const InstanceInfo: Story = {
|
||||||
|
parameters: {
|
||||||
|
routePath: '/hosting/manage/demo-server/instances',
|
||||||
|
resetInstanceInfoAdmonition: true,
|
||||||
|
serverWorlds: [{ id: 'demo-world' }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user