feat(web): 统一工作流焦点与播报
This commit is contained in:
14 files changed
+163
-28
No files matched your search
@@ -46,7 +46,7 @@ describe('AccessKeyDeleteDialog', () => {
|
||||
method: 'DELETE',
|
||||
signal: expect.any(AbortSignal),
|
||||
})
|
||||
expect(wrapper.emitted('deleted')).toEqual([[]])
|
||||
expect(wrapper.emitted('deleted')).toEqual([['client']])
|
||||
expect(invalidate).not.toHaveBeenCalled()
|
||||
expect(client.getMutationCache().getAll()).toHaveLength(0)
|
||||
wrapper.unmount()
|
||||
@@ -108,7 +108,7 @@ describe('AccessKeyDeleteDialog', () => {
|
||||
|
||||
resolveDelete()
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('deleted')).toEqual([[]])
|
||||
expect(wrapper.emitted('deleted')).toEqual([['client']])
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,7 @@ import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
|
||||
const props = defineProps<{ accessKey: AccessKeyDto; total: number }>()
|
||||
const emit = defineEmits<{ deleted: [] }>()
|
||||
const emit = defineEmits<{ deleted: [name: string] }>()
|
||||
const client = useApiClient()
|
||||
const { t } = useI18n()
|
||||
const open = ref(false)
|
||||
@@ -39,7 +39,7 @@ async function confirmDelete(): Promise<void> {
|
||||
try {
|
||||
await deleteAccessKey(client, props.accessKey.id, activeController.signal)
|
||||
open.value = false
|
||||
emit('deleted')
|
||||
emit('deleted', props.accessKey.name)
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof RequestCancelledError)) failed.value = true
|
||||
} finally {
|
||||
|
||||
@@ -15,7 +15,10 @@ import AccessKeyDeleteDialog from './AccessKeyDeleteDialog.vue'
|
||||
import { useEphemeralSecret } from './use-ephemeral-secret'
|
||||
|
||||
const props = defineProps<{ accessKeys: AccessKeyDto[]; groups: GroupSummary[] }>()
|
||||
const emit = defineEmits<{ edit: [accessKey: AccessKeyDto, trigger: HTMLElement]; deleted: [] }>()
|
||||
const emit = defineEmits<{
|
||||
edit: [accessKey: AccessKeyDto, trigger: HTMLElement]
|
||||
deleted: [name: string]
|
||||
}>()
|
||||
const client = useApiClient()
|
||||
const { locale, t } = useI18n()
|
||||
const secret = useEphemeralSecret()
|
||||
@@ -179,7 +182,7 @@ function rpmSummary(rpm: number): string {
|
||||
<AccessKeyDeleteDialog
|
||||
:access-key="accessKey"
|
||||
:total="accessKeys.length"
|
||||
@deleted="emit('deleted')"
|
||||
@deleted="emit('deleted', $event)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -183,6 +183,9 @@ describe('AccessKeysView', () => {
|
||||
expect(document.activeElement).toBe(
|
||||
wrapper.get('button[data-test="access-key-create"]').element,
|
||||
)
|
||||
const announcement = wrapper.get('[data-test="access-key-delete-announcement"]')
|
||||
expect(announcement.attributes('aria-live')).toBe('polite')
|
||||
expect(announcement.text()).toContain('Deleted AccessKey “client”.')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -292,7 +295,7 @@ describe('AccessKeysView', () => {
|
||||
).element
|
||||
const focus = vi.spyOn(createButton, 'focus')
|
||||
|
||||
wrapper.getComponent(AccessKeyTable).vm.$emit('deleted')
|
||||
wrapper.getComponent(AccessKeyTable).vm.$emit('deleted', 'client')
|
||||
wrapper.unmount()
|
||||
await nextTick()
|
||||
await flushPromises()
|
||||
|
||||
@@ -27,6 +27,7 @@ const drawerOpen = ref(false)
|
||||
const selected = ref<AccessKeyDto | null>(null)
|
||||
const createOperation = ref<PendingAccessKeyCreateOperation | null>(null)
|
||||
const viewRoot = ref<HTMLElement | null>(null)
|
||||
const deletionAnnouncement = ref('')
|
||||
let restoreFocus: HTMLElement | null = null
|
||||
let mounted = true
|
||||
|
||||
@@ -88,7 +89,9 @@ async function setDrawerOpen(open: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function focusCreateAfterDelete(): Promise<void> {
|
||||
async function focusCreateAfterDelete(name: string): Promise<void> {
|
||||
deletionAnnouncement.value = ''
|
||||
await nextTick()
|
||||
await Promise.all(
|
||||
accessKeyMutationInvalidations.delete.map((queryKey) =>
|
||||
queryClient.invalidateQueries({ queryKey, exact: true }),
|
||||
@@ -96,6 +99,7 @@ async function focusCreateAfterDelete(): Promise<void> {
|
||||
)
|
||||
await nextTick()
|
||||
if (!mounted) return
|
||||
deletionAnnouncement.value = t('accessKeys.delete.deletedAnnouncement', { name })
|
||||
const target = viewRoot.value?.querySelector('button[data-test="access-key-create"]')
|
||||
if (target instanceof HTMLButtonElement && target.isConnected) target.focus()
|
||||
}
|
||||
@@ -143,6 +147,15 @@ async function focusCreateAfterDelete(): Promise<void> {
|
||||
</AppButton>
|
||||
</section>
|
||||
|
||||
<p
|
||||
class="sr-only"
|
||||
data-test="access-key-delete-announcement"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{{ deletionAnnouncement }}
|
||||
</p>
|
||||
|
||||
<QueryFeedback
|
||||
v-if="accessKeysQuery.isPending.value"
|
||||
state="loading"
|
||||
|
||||
@@ -54,6 +54,7 @@ async function mountExisting(
|
||||
path: string,
|
||||
request: ApiClient['request'],
|
||||
initialDraft?: ExistingGroupImportDraft,
|
||||
attachTo?: Element,
|
||||
) {
|
||||
const client = queryClient()
|
||||
const importRecovery = recovery()
|
||||
@@ -65,13 +66,43 @@ async function mountExisting(
|
||||
locale: 'en-US',
|
||||
recovery: importRecovery,
|
||||
operationOwner,
|
||||
mounting: { props: { initialDraft } },
|
||||
mounting: { props: { initialDraft }, attachTo },
|
||||
})
|
||||
await flushPromises()
|
||||
return { ...mounted, importRecovery, operationOwner, queryClient: client }
|
||||
}
|
||||
|
||||
describe('ExistingGroupImport', () => {
|
||||
it('focuses the review heading and then the confirmed result heading', async () => {
|
||||
const request = vi.fn(async (path: string) => {
|
||||
if (path === '/api/groups') return groups
|
||||
if (path === '/api/groups/7/keys/import') {
|
||||
return { group_id: 7, keys_added: 1, keys_duplicated: 0 }
|
||||
}
|
||||
throw new Error(`unexpected request: ${path}`)
|
||||
}) as ApiClient['request']
|
||||
const { wrapper } = await mountExisting(
|
||||
'/import?mode=existing&group_id=7',
|
||||
request,
|
||||
undefined,
|
||||
document.body,
|
||||
)
|
||||
|
||||
await wrapper.get('[data-test="keys"]').setValue('raw-key')
|
||||
await wrapper.get('[data-test="existing-review"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(
|
||||
wrapper.get('[data-test="existing-review-heading"]').element,
|
||||
)
|
||||
|
||||
await wrapper.get('[data-test="existing-submit"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(
|
||||
wrapper.get('[data-test="existing-result-heading"]').element,
|
||||
)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('preselects a positive route Group and imports exact raw keys without discovery or Group mutation', async () => {
|
||||
const requestMock = vi.fn(async (path: string, _options?: ApiRequestOptions) => {
|
||||
void _options
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { Check, ChevronLeft } from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -32,6 +32,8 @@ const reviewing = ref(false)
|
||||
const completed = ref(false)
|
||||
const errorKey = ref('')
|
||||
const result = ref<GroupKeyImportResult | null>(null)
|
||||
const reviewHeading = ref<HTMLHeadingElement | null>(null)
|
||||
const resultHeading = ref<HTMLHeadingElement | null>(null)
|
||||
const importOperationOwner = useImportOperationOwner()
|
||||
const operation = importOperationOwner.importKeys
|
||||
if (operation.outcome.value?.kind === 'confirmed') operation.reset()
|
||||
@@ -101,10 +103,12 @@ function selectGroup(event: Event): void {
|
||||
void router.push({ name: 'import', query })
|
||||
}
|
||||
|
||||
function showReview(): void {
|
||||
async function showReview(): Promise<void> {
|
||||
if (!canReview.value) return
|
||||
errorKey.value = ''
|
||||
reviewing.value = true
|
||||
await nextTick()
|
||||
reviewHeading.value?.focus()
|
||||
}
|
||||
|
||||
function returnToEdit(): void {
|
||||
@@ -160,6 +164,8 @@ async function executeImportOperation(): Promise<void> {
|
||||
completed.value = true
|
||||
keys.value = ''
|
||||
recovery.clear()
|
||||
await nextTick()
|
||||
resultHeading.value?.focus()
|
||||
return
|
||||
}
|
||||
if (!componentActive) return
|
||||
@@ -241,7 +247,14 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template v-else>
|
||||
<section class="review" :aria-labelledby="'existing-review-title'">
|
||||
<h3 id="existing-review-title">{{ t('import.existing.reviewTitle') }}</h3>
|
||||
<h3
|
||||
id="existing-review-title"
|
||||
ref="reviewHeading"
|
||||
data-test="existing-review-heading"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{ t('import.existing.reviewTitle') }}
|
||||
</h3>
|
||||
<p>{{ t('import.existing.reviewDescription') }}</p>
|
||||
<dl>
|
||||
<div>
|
||||
@@ -273,7 +286,9 @@ onBeforeUnmount(() => {
|
||||
<section v-else data-test="existing-result" class="result" role="status" aria-live="polite">
|
||||
<Check :size="22" aria-hidden="true" />
|
||||
<div>
|
||||
<h3>{{ t('import.existing.successTitle') }}</h3>
|
||||
<h3 ref="resultHeading" data-test="existing-result-heading" tabindex="-1">
|
||||
{{ t('import.existing.successTitle') }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
t('import.existing.successSummary', {
|
||||
|
||||
@@ -30,7 +30,11 @@ function recovery(): ImportRecoveryService {
|
||||
}
|
||||
}
|
||||
|
||||
async function mountImport(request: ApiClient['request'], initialDraft?: ImportDraft) {
|
||||
async function mountImport(
|
||||
request: ApiClient['request'],
|
||||
initialDraft?: ImportDraft,
|
||||
attachTo?: Element,
|
||||
) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
@@ -41,6 +45,7 @@ async function mountImport(request: ApiClient['request'], initialDraft?: ImportD
|
||||
const operationOwner = createImportOperationOwner()
|
||||
const wrapper = mount(NewGroupImport, {
|
||||
props: { initialDraft },
|
||||
attachTo,
|
||||
global: {
|
||||
plugins: [
|
||||
router,
|
||||
@@ -73,6 +78,31 @@ async function discoverAndReview(wrapper: ReturnType<typeof mount>) {
|
||||
}
|
||||
|
||||
describe('NewGroupImport', () => {
|
||||
it('focuses the new heading after every explicit step transition', async () => {
|
||||
const request = vi.fn().mockResolvedValue({ models: ['gpt-4o'] }) as ApiClient['request']
|
||||
const { wrapper } = await mountImport(request, undefined, document.body)
|
||||
await enterConnection(wrapper)
|
||||
|
||||
await wrapper.get('[data-test="discover"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(wrapper.get('[data-test="import-step-2-heading"]').element)
|
||||
|
||||
await wrapper.get('[data-test="review"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(wrapper.get('[data-test="import-step-3-heading"]').element)
|
||||
|
||||
const reviewBack = wrapper.findAll('button').find((button) => button.text() === 'Back')
|
||||
await reviewBack?.trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(wrapper.get('[data-test="import-step-2-heading"]').element)
|
||||
|
||||
const modelsBack = wrapper.findAll('button').find((button) => button.text() === 'Back')
|
||||
await modelsBack?.trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.activeElement).toBe(wrapper.get('[data-test="import-step-1-heading"]').element)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('disables conflict Edit while append is pending', async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { Check, ChevronLeft, ChevronRight, Search, TriangleAlert } from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -71,6 +71,9 @@ const manualMode = ref(
|
||||
const errorKey = ref('')
|
||||
const conflict = ref<UpstreamUrlConflictData | null>(null)
|
||||
const completed = ref(false)
|
||||
const step1Heading = ref<HTMLHeadingElement | null>(null)
|
||||
const step2Heading = ref<HTMLHeadingElement | null>(null)
|
||||
const step3Heading = ref<HTMLHeadingElement | null>(null)
|
||||
const importOperationOwner = useImportOperationOwner()
|
||||
const createOperation = importOperationOwner.createGroup
|
||||
const appendOperation = importOperationOwner.importKeys
|
||||
@@ -181,6 +184,14 @@ function invalidateDiscovery(): void {
|
||||
if (draft.step > 1) draft.step = 1
|
||||
}
|
||||
|
||||
async function moveToStep(step: ImportDraft['step']): Promise<void> {
|
||||
draft.step = step
|
||||
await nextTick()
|
||||
const heading =
|
||||
step === 1 ? step1Heading.value : step === 2 ? step2Heading.value : step3Heading.value
|
||||
heading?.focus()
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
() => draft.upstream_url,
|
||||
@@ -225,12 +236,12 @@ async function runDiscovery(): Promise<void> {
|
||||
discoveryReady.value = true
|
||||
discoveryFailed.value = false
|
||||
manualMode.value = true
|
||||
draft.step = 2
|
||||
await moveToStep(2)
|
||||
} catch (error: unknown) {
|
||||
if (discoveryController !== controller || error instanceof RequestCancelledError) return
|
||||
discoveryFailed.value = true
|
||||
manualMode.value = false
|
||||
draft.step = 2
|
||||
await moveToStep(2)
|
||||
errorKey.value = 'import.discoveryFailed'
|
||||
} finally {
|
||||
finishAction(controller)
|
||||
@@ -349,11 +360,11 @@ async function retryOperation(): Promise<void> {
|
||||
if (createOperation.operation.value) await submitCreate()
|
||||
}
|
||||
|
||||
function returnToEdit(): void {
|
||||
async function returnToEdit(): Promise<void> {
|
||||
createOperation.reset()
|
||||
appendOperation.reset()
|
||||
conflict.value = null
|
||||
draft.step = 1
|
||||
await moveToStep(1)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -405,7 +416,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<SurfaceCard v-if="draft.step === 1" class="import-card">
|
||||
<header>
|
||||
<h2>{{ t('import.connection.title') }}</h2>
|
||||
<h2 ref="step1Heading" data-test="import-step-1-heading" tabindex="-1">
|
||||
{{ t('import.connection.title') }}
|
||||
</h2>
|
||||
<p>{{ t('import.connection.description') }}</p>
|
||||
</header>
|
||||
<div class="connection-grid">
|
||||
@@ -465,7 +478,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<SurfaceCard v-else-if="draft.step === 2" class="import-card">
|
||||
<header>
|
||||
<h2>{{ t('import.models.title') }}</h2>
|
||||
<h2 ref="step2Heading" data-test="import-step-2-heading" tabindex="-1">
|
||||
{{ t('import.models.title') }}
|
||||
</h2>
|
||||
<p>{{ t('import.models.stepDescription') }}</p>
|
||||
</header>
|
||||
<InlineFeedback v-if="errorKey" tone="warning">{{ t(errorKey) }}</InlineFeedback>
|
||||
@@ -480,9 +495,9 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<ModelDraftEditor v-if="manualMode" v-model="draft.models" />
|
||||
<footer class="card-actions split">
|
||||
<AppButton variant="secondary" @click="draft.step = 1"
|
||||
<AppButton variant="secondary" @click="moveToStep(1)"
|
||||
><ChevronLeft :size="16" aria-hidden="true" />{{ t('import.back') }}</AppButton
|
||||
><AppButton data-test="review" :disabled="!canReview" @click="draft.step = 3"
|
||||
><AppButton data-test="review" :disabled="!canReview" @click="moveToStep(3)"
|
||||
>{{ t('import.review') }}<ChevronRight :size="16" aria-hidden="true"
|
||||
/></AppButton>
|
||||
</footer>
|
||||
@@ -490,7 +505,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<SurfaceCard v-else class="import-card">
|
||||
<header>
|
||||
<h2>{{ t('import.reviewTitle') }}</h2>
|
||||
<h2 ref="step3Heading" data-test="import-step-3-heading" tabindex="-1">
|
||||
{{ t('import.reviewTitle') }}
|
||||
</h2>
|
||||
<p>{{ t('import.reviewDescription') }}</p>
|
||||
</header>
|
||||
<dl class="review-list">
|
||||
@@ -553,7 +570,7 @@ onBeforeUnmount(() => {
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
:disabled="pending || operationNoticeKey !== ''"
|
||||
@click="draft.step = 2"
|
||||
@click="moveToStep(2)"
|
||||
><ChevronLeft :size="16" aria-hidden="true" />{{ t('import.back') }}</AppButton
|
||||
><AppButton
|
||||
data-test="create"
|
||||
|
||||
@@ -197,6 +197,8 @@ describe('InspectorTab', () => {
|
||||
'/monitor?tab=inspector&protocol=anthropic&external_model=claude-exact&access_key_id=13&raw_access_key=sk-gl-route-canary',
|
||||
)
|
||||
|
||||
const submit = wrapper.get<HTMLButtonElement>('[data-test="inspector-submit"]').element
|
||||
submit.focus()
|
||||
await wrapper.get('[data-test="inspector-form"]').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
@@ -224,6 +226,8 @@ describe('InspectorTab', () => {
|
||||
expect(wrapper.get('[data-test="inspector-access-key-status"]').classes()).toContain(
|
||||
'status-badge--neutral',
|
||||
)
|
||||
expect(wrapper.get('[data-test="inspector-result"]').attributes('aria-live')).toBe('polite')
|
||||
expect(document.activeElement).toBe(submit)
|
||||
})
|
||||
|
||||
it('requires reselection when a deep-linked AccessKey no longer exists', async () => {
|
||||
@@ -598,6 +602,9 @@ describe('InspectorTab', () => {
|
||||
'complete current-runtime explanation',
|
||||
)
|
||||
expect(wrapper.find('[data-test="inspector-request-error"]').exists()).toBe(false)
|
||||
expect(document.activeElement).toBe(
|
||||
wrapper.get('[data-test="inspector-result-summary"]').element,
|
||||
)
|
||||
})
|
||||
|
||||
it('maps all 14 reason codes and uses a safe fallback for an unknown code', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -56,6 +56,7 @@ const failed = ref(false)
|
||||
const resultStale = ref(false)
|
||||
const submitted = ref<RouteInspectRequest>()
|
||||
const observation = ref<RouteInspectResponseDto>()
|
||||
const resultSummary = ref<HTMLHeadingElement | null>(null)
|
||||
let owner = 0
|
||||
let controller: AbortController | undefined
|
||||
|
||||
@@ -217,6 +218,10 @@ async function inspect(): Promise<void> {
|
||||
submitted.value = request
|
||||
observation.value = result
|
||||
resultStale.value = false
|
||||
await nextTick()
|
||||
if (currentOwner === owner && !currentController.signal.aborted && !result.routable) {
|
||||
resultSummary.value?.focus()
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
@@ -393,7 +398,13 @@ onBeforeUnmount(() => {
|
||||
@retry="inspect"
|
||||
/>
|
||||
|
||||
<SurfaceCard v-if="observation" class="inspector-result" data-test="inspector-result">
|
||||
<SurfaceCard
|
||||
v-if="observation"
|
||||
class="inspector-result"
|
||||
data-test="inspector-result"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<p
|
||||
v-if="inputChanged"
|
||||
class="inspector-input-changed"
|
||||
@@ -413,7 +424,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<header class="inspector-heading inspector-result__heading">
|
||||
<div>
|
||||
<h2>{{ t('monitor.inspector.result.title') }}</h2>
|
||||
<h2 ref="resultSummary" data-test="inspector-result-summary" tabindex="-1">
|
||||
{{ t('monitor.inspector.result.title') }}
|
||||
</h2>
|
||||
<p>{{ t('monitor.inspector.boundary') }}</p>
|
||||
</div>
|
||||
<StatusBadge
|
||||
|
||||
@@ -91,6 +91,7 @@ export default {
|
||||
'This is the last AccessKey. Deleting it leaves data-plane clients without an issued credential.',
|
||||
confirm: 'Delete AccessKey',
|
||||
failed: 'Unable to delete the AccessKey.',
|
||||
deletedAnnouncement: 'Deleted AccessKey “{name}”.',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -92,6 +92,7 @@ export default {
|
||||
'これは最後の AccessKey です。削除すると、データプレーンクライアントに発行済み資格情報がなくなります。',
|
||||
confirm: 'AccessKey を削除',
|
||||
failed: 'AccessKey を削除できません。',
|
||||
deletedAnnouncement: 'AccessKey「{name}」を削除しました。',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
lastWarning: '这是最后一个 AccessKey。删除后,数据面客户端将没有已签发凭据。',
|
||||
confirm: '删除 AccessKey',
|
||||
failed: '无法删除 AccessKey。',
|
||||
deletedAnnouncement: '已删除 AccessKey“{name}”。',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
Reference in new issue
Block a user