Commit a72e5082 by Seefs Committed by GitHub

feat(system-info): add stale instance cleanup actions (#5953)

parent fc1259f5
...@@ -2,6 +2,7 @@ package controller ...@@ -2,6 +2,7 @@ package controller
import ( import (
"net/http" "net/http"
"strings"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
...@@ -28,3 +29,37 @@ func ListSystemInstances(c *gin.Context) { ...@@ -28,3 +29,37 @@ func ListSystemInstances(c *gin.Context) {
"data": responses, "data": responses,
}) })
} }
func DeleteStaleSystemInstances(c *gin.Context) {
deletedCount, err := model.DeleteStaleSystemInstances(common.GetTimestamp())
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, gin.H{
"deleted_count": deletedCount,
})
}
func DeleteStaleSystemInstance(c *gin.Context) {
nodeName := c.Param("node_name")
if strings.TrimSpace(nodeName) == "" {
common.ApiErrorMsg(c, "node name is required")
return
}
deleted, err := model.DeleteStaleSystemInstance(nodeName, common.GetTimestamp())
if err != nil {
common.ApiError(c, err)
return
}
if !deleted {
common.ApiErrorMsg(c, "instance is not stale or no longer exists")
return
}
common.ApiSuccess(c, gin.H{
"deleted_count": 1,
})
}
...@@ -75,6 +75,16 @@ func ListSystemInstances() ([]*SystemInstance, error) { ...@@ -75,6 +75,16 @@ func ListSystemInstances() ([]*SystemInstance, error) {
return instances, err return instances, err
} }
func DeleteStaleSystemInstances(now int64) (int64, error) {
result := DB.Where("last_seen_at < ?", now-SystemInstanceStaleAfterSeconds).Delete(&SystemInstance{})
return result.RowsAffected, result.Error
}
func DeleteStaleSystemInstance(nodeName string, now int64) (bool, error) {
result := DB.Where("node_name = ? AND last_seen_at < ?", nodeName, now-SystemInstanceStaleAfterSeconds).Delete(&SystemInstance{})
return result.RowsAffected > 0, result.Error
}
func (instance *SystemInstance) ToResponse(now int64) SystemInstanceResponse { func (instance *SystemInstance) ToResponse(now int64) SystemInstanceResponse {
status := SystemInstanceStatusOnline status := SystemInstanceStatusOnline
if now-instance.LastSeenAt > SystemInstanceStaleAfterSeconds { if now-instance.LastSeenAt > SystemInstanceStaleAfterSeconds {
......
...@@ -288,6 +288,8 @@ func SetApiRouter(router *gin.Engine) { ...@@ -288,6 +288,8 @@ func SetApiRouter(router *gin.Engine) {
systemInfoRoute.Use(middleware.RootAuth()) systemInfoRoute.Use(middleware.RootAuth())
{ {
systemInfoRoute.GET("/instances", controller.ListSystemInstances) systemInfoRoute.GET("/instances", controller.ListSystemInstances)
systemInfoRoute.DELETE("/stale-instances", controller.DeleteStaleSystemInstances)
systemInfoRoute.DELETE("/instances/:node_name", controller.DeleteStaleSystemInstance)
} }
dataRoute := apiRouter.Group("/data") dataRoute := apiRouter.Group("/data")
......
...@@ -18,7 +18,10 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,7 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { api } from '@/lib/api' import { api } from '@/lib/api'
import type { SystemInstanceListResponse } from './types' import type {
SystemInstanceDeleteResponse,
SystemInstanceListResponse,
} from './types'
export async function listSystemInstances() { export async function listSystemInstances() {
const res = await api.get<SystemInstanceListResponse>( const res = await api.get<SystemInstanceListResponse>(
...@@ -26,3 +29,17 @@ export async function listSystemInstances() { ...@@ -26,3 +29,17 @@ export async function listSystemInstances() {
) )
return res.data return res.data
} }
export async function deleteStaleSystemInstances() {
const res = await api.delete<SystemInstanceDeleteResponse>(
'/api/system-info/stale-instances'
)
return res.data
}
export async function deleteStaleSystemInstance(nodeName: string) {
const res = await api.delete<SystemInstanceDeleteResponse>(
`/api/system-info/instances/${encodeURIComponent(nodeName)}`
)
return res.data
}
...@@ -16,11 +16,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,11 +16,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useQuery } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, RefreshCw, ServerCog } from 'lucide-react' import {
import type { ReactNode } from 'react' AlertTriangle,
Loader2,
RefreshCw,
ServerCog,
Trash2,
} from 'lucide-react'
import { useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { ErrorState } from '@/components/error-state' import { ErrorState } from '@/components/error-state'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
...@@ -51,10 +59,19 @@ import { toIntlLocale } from '@/i18n/languages' ...@@ -51,10 +59,19 @@ import { toIntlLocale } from '@/i18n/languages'
import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { listSystemInstances } from '../api' import {
deleteStaleSystemInstance,
deleteStaleSystemInstances,
listSystemInstances,
} from '../api'
import type { SystemInstance, SystemInstanceStatus } from '../types' import type { SystemInstance, SystemInstanceStatus } from '../types'
const INSTANCE_POLL_INTERVAL_MS = 30_000 const INSTANCE_POLL_INTERVAL_MS = 30_000
const INSTANCE_SKELETON_KEYS = [
'system-instance-skeleton-1',
'system-instance-skeleton-2',
'system-instance-skeleton-3',
]
const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = { const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = {
online: online:
...@@ -210,6 +227,9 @@ function ResourceCell(props: ResourceCellProps) { ...@@ -210,6 +227,9 @@ function ResourceCell(props: ResourceCellProps) {
type SystemInstancesTableProps = { type SystemInstancesTableProps = {
instances: SystemInstance[] instances: SystemInstance[]
deletingNodeName: string | null
isDeletingInstance: boolean
onDeleteStaleInstance: (instance: SystemInstance) => void
} }
function SystemInstancesList(props: SystemInstancesTableProps) { function SystemInstancesList(props: SystemInstancesTableProps) {
...@@ -217,7 +237,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -217,7 +237,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
return ( return (
<div className='overflow-x-auto rounded-md border'> <div className='overflow-x-auto rounded-md border'>
<Table className='min-w-[1140px]'> <Table className='min-w-[1230px]'>
<TableHeader> <TableHeader>
<TableRow className='bg-muted/40 hover:bg-muted/40'> <TableRow className='bg-muted/40 hover:bg-muted/40'>
<TableHead className='h-9 min-w-[240px] px-4 text-xs'> <TableHead className='h-9 min-w-[240px] px-4 text-xs'>
...@@ -243,9 +263,12 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -243,9 +263,12 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
<TableHead className='h-9 w-[170px] text-xs'> <TableHead className='h-9 w-[170px] text-xs'>
{t('Started')} {t('Started')}
</TableHead> </TableHead>
<TableHead className='h-9 w-[170px] pr-4 text-xs'> <TableHead className='h-9 w-[170px] text-xs'>
{t('Last Seen')} {t('Last Seen')}
</TableHead> </TableHead>
<TableHead className='h-9 w-[90px] pr-4 text-right text-xs'>
{t('Actions')}
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
...@@ -254,6 +277,9 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -254,6 +277,9 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
instance.info?.node?.should_configure_manually === true instance.info?.node?.should_configure_manually === true
const resources = instance.info?.resources const resources = instance.info?.resources
const storage = resources?.storage const storage = resources?.storage
const isDeletingThisInstance =
props.isDeletingInstance &&
props.deletingNodeName === instance.node_name
return ( return (
<TableRow key={instance.node_name} className='hover:bg-muted/30'> <TableRow key={instance.node_name} className='hover:bg-muted/30'>
<TableCell className='px-4 py-2.5 align-middle'> <TableCell className='px-4 py-2.5 align-middle'>
...@@ -406,7 +432,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -406,7 +432,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
{formatTimestampToDate(instance.started_at)} {formatTimestampToDate(instance.started_at)}
</TableCell> </TableCell>
<TableCell <TableCell
className='text-muted-foreground py-2.5 pr-4 align-middle text-xs whitespace-nowrap' className='text-muted-foreground py-2.5 align-middle text-xs whitespace-nowrap'
title={formatTimestampToDate(instance.last_seen_at)} title={formatTimestampToDate(instance.last_seen_at)}
> >
{formatTimestampRelative( {formatTimestampRelative(
...@@ -415,6 +441,45 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -415,6 +441,45 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
toIntlLocale(i18n.language) toIntlLocale(i18n.language)
)} )}
</TableCell> </TableCell>
<TableCell className='py-2.5 pr-4 text-right align-middle'>
{instance.status === 'stale' ? (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<Button
type='button'
variant='destructive'
size='icon-xs'
onClick={() =>
props.onDeleteStaleInstance(instance)
}
disabled={
props.isDeletingInstance ||
isDeletingThisInstance
}
aria-label={t('Delete stale instance')}
>
{isDeletingThisInstance ? (
<Loader2
className='size-3 animate-spin'
aria-hidden='true'
/>
) : (
<Trash2 className='size-3' aria-hidden='true' />
)}
</Button>
}
/>
<TooltipContent>
{t('Delete stale instance')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className='text-muted-foreground text-xs'>-</span>
)}
</TableCell>
</TableRow> </TableRow>
) )
})} })}
...@@ -426,6 +491,10 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -426,6 +491,10 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
export function SystemInstancesPanel() { export function SystemInstancesPanel() {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient()
const [deleteTarget, setDeleteTarget] = useState<SystemInstance | null>(null)
const [deleteAllConfirmOpen, setDeleteAllConfirmOpen] = useState(false)
const [deletingNodeName, setDeletingNodeName] = useState<string | null>(null)
const instancesQuery = useQuery({ const instancesQuery = useQuery({
queryKey: ['system-info', 'instances'], queryKey: ['system-info', 'instances'],
queryFn: async () => { queryFn: async () => {
...@@ -441,89 +510,235 @@ export function SystemInstancesPanel() { ...@@ -441,89 +510,235 @@ export function SystemInstancesPanel() {
}) })
const instances = instancesQuery.data ?? [] const instances = instancesQuery.data ?? []
const staleInstances = instances.filter(
(instance) => instance.status === 'stale'
)
const loading = instancesQuery.isLoading const loading = instancesQuery.isLoading
const refreshing = instancesQuery.isFetching && !instancesQuery.isLoading const refreshing = instancesQuery.isFetching && !instancesQuery.isLoading
return ( const invalidateInstances = async () => {
<section className='bg-card overflow-hidden rounded-lg border shadow-xs'> await queryClient.invalidateQueries({
<div className='flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5'> queryKey: ['system-info', 'instances'],
<div className='min-w-0'> })
<div className='flex items-center gap-2'> }
<span className='bg-muted text-muted-foreground inline-flex size-7 items-center justify-center rounded-md'>
<ServerCog className='size-4' aria-hidden='true' /> const deleteStaleInstanceMutation = useMutation({
</span> mutationFn: async (nodeName: string) => {
<div className='min-w-0'> const res = await deleteStaleSystemInstance(nodeName)
<h3 className='text-sm font-semibold'>{t('Instances')}</h3> if (!res.success) {
<p className='text-muted-foreground mt-0.5 text-xs'> throw new Error(res.message || t('Delete failed'))
{t( }
'Nodes reporting from this deployment and their latest heartbeat.' return res
)} },
</p> onMutate: (nodeName) => {
</div> setDeletingNodeName(nodeName)
</div> },
</div> onSuccess: async () => {
<div className='flex shrink-0 items-center gap-3'> toast.success(t('Deleted stale instance'))
<span className='text-muted-foreground text-xs' aria-live='polite'> await invalidateInstances()
{t('Auto-refreshing every {{seconds}}s', { setDeleteTarget(null)
seconds: INSTANCE_POLL_INTERVAL_MS / 1000, },
})} onError: (error) => {
</span> toast.error(error instanceof Error ? error.message : t('Delete failed'))
<Button void invalidateInstances()
type='button' },
variant='outline' onSettled: () => {
size='sm' setDeletingNodeName(null)
onClick={() => void instancesQuery.refetch()} },
disabled={instancesQuery.isFetching} })
aria-label={t('Refresh')}
> const deleteStaleInstancesMutation = useMutation({
<RefreshCw mutationFn: async () => {
data-icon='inline-start' const res = await deleteStaleSystemInstances()
className={cn('size-3.5', refreshing && 'animate-spin')} if (!res.success) {
aria-hidden='true' throw new Error(res.message || t('Delete failed'))
/> }
{refreshing ? t('Refreshing...') : t('Refresh')} return res
</Button> },
onSuccess: async (res) => {
toast.success(
t('Deleted {{count}} stale instances', {
count: res.data?.deleted_count ?? 0,
})
)
await invalidateInstances()
setDeleteAllConfirmOpen(false)
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : t('Delete failed'))
},
})
const isMutatingInstance =
deletingNodeName !== null || deleteStaleInstanceMutation.isPending
let instancesContent: ReactNode
if (loading) {
instancesContent = (
<div className='space-y-2 p-4 sm:p-5'>
{INSTANCE_SKELETON_KEYS.map((key) => (
<Skeleton key={key} className='h-9 w-full rounded-md' />
))}
</div>
)
} else if (instancesQuery.isError) {
instancesContent = (
<ErrorState
title={t('We could not load instances.')}
description={
instancesQuery.error instanceof Error
? instancesQuery.error.message
: undefined
}
onRetry={() => {
void instancesQuery.refetch()
}}
className='min-h-[220px]'
/>
)
} else if (instances.length === 0) {
instancesContent = (
<div className='px-4 py-10 text-center sm:px-5'>
<div className='bg-muted mx-auto mb-3 flex size-10 items-center justify-center rounded-lg'>
<ServerCog
className='text-muted-foreground size-5'
aria-hidden='true'
/>
</div> </div>
<p className='text-muted-foreground text-sm'>
{t('No instances have reported yet.')}
</p>
</div>
)
} else {
instancesContent = (
<div className='p-4 sm:p-5'>
<SystemInstancesList
instances={instances}
deletingNodeName={deletingNodeName}
isDeletingInstance={
isMutatingInstance || deleteStaleInstancesMutation.isPending
}
onDeleteStaleInstance={setDeleteTarget}
/>
</div> </div>
)
}
<div aria-busy={instancesQuery.isFetching}> return (
{loading ? ( <>
<div className='space-y-2 p-4 sm:p-5'> <section className='bg-card overflow-hidden rounded-lg border shadow-xs'>
{Array.from({ length: 3 }).map((_, i) => ( <div className='flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5'>
<Skeleton key={i} className='h-9 w-full rounded-md' /> <div className='min-w-0'>
))} <div className='flex items-center gap-2'>
<span className='bg-muted text-muted-foreground inline-flex size-7 items-center justify-center rounded-md'>
<ServerCog className='size-4' aria-hidden='true' />
</span>
<div className='min-w-0'>
<h3 className='text-sm font-semibold'>{t('Instances')}</h3>
<p className='text-muted-foreground mt-0.5 text-xs'>
{t(
'Nodes reporting from this deployment and their latest heartbeat.'
)}
</p>
</div>
</div>
</div> </div>
) : instancesQuery.isError ? ( <div className='flex shrink-0 flex-wrap items-center gap-2 sm:justify-end'>
<ErrorState <span className='text-muted-foreground text-xs' aria-live='polite'>
title={t('We could not load instances.')} {t('Auto-refreshing every {{seconds}}s', {
description={ seconds: INSTANCE_POLL_INTERVAL_MS / 1000,
instancesQuery.error instanceof Error })}
? instancesQuery.error.message </span>
: undefined <Button
} type='button'
onRetry={() => { variant='destructive'
void instancesQuery.refetch() size='sm'
}} onClick={() => setDeleteAllConfirmOpen(true)}
className='min-h-[220px]' disabled={
/> staleInstances.length === 0 ||
) : instances.length === 0 ? ( isMutatingInstance ||
<div className='px-4 py-10 text-center sm:px-5'> deleteStaleInstancesMutation.isPending
<div className='bg-muted mx-auto mb-3 flex size-10 items-center justify-center rounded-lg'> }
<ServerCog >
className='text-muted-foreground size-5' {deleteStaleInstancesMutation.isPending ? (
<Loader2
data-icon='inline-start'
className='size-3.5 animate-spin'
aria-hidden='true'
/>
) : (
<Trash2
data-icon='inline-start'
className='size-3.5'
aria-hidden='true'
/>
)}
{t('Delete all stale')}
</Button>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => void instancesQuery.refetch()}
disabled={instancesQuery.isFetching}
aria-label={t('Refresh')}
>
<RefreshCw
data-icon='inline-start'
className={cn('size-3.5', refreshing && 'animate-spin')}
aria-hidden='true' aria-hidden='true'
/> />
</div> {refreshing ? t('Refreshing...') : t('Refresh')}
<p className='text-muted-foreground text-sm'> </Button>
{t('No instances have reported yet.')}
</p>
</div>
) : (
<div className='p-4 sm:p-5'>
<SystemInstancesList instances={instances} />
</div> </div>
</div>
<div aria-busy={instancesQuery.isFetching}>{instancesContent}</div>
</section>
<ConfirmDialog
open={deleteAllConfirmOpen}
onOpenChange={setDeleteAllConfirmOpen}
title={t('Delete stale instances')}
desc={t(
'Delete {{count}} stale instance records? Online instances will not be deleted.',
{ count: staleInstances.length }
)} )}
</div> destructive
</section> isLoading={deleteStaleInstancesMutation.isPending}
confirmText={
deleteStaleInstancesMutation.isPending
? t('Deleting...')
: t('Delete')
}
handleConfirm={() => deleteStaleInstancesMutation.mutate()}
/>
<ConfirmDialog
open={deleteTarget !== null}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null)
}}
title={t('Delete stale instance')}
desc={
deleteTarget
? t(
'Delete stale instance "{{name}}"? If it has reported again, it will not be deleted.',
{ name: getNodeName(deleteTarget) }
)
: ''
}
destructive
isLoading={deleteStaleInstanceMutation.isPending}
confirmText={
deleteStaleInstanceMutation.isPending ? t('Deleting...') : t('Delete')
}
handleConfirm={() => {
if (!deleteTarget) return
deleteStaleInstanceMutation.mutate(deleteTarget.node_name)
}}
/>
</>
) )
} }
...@@ -77,3 +77,11 @@ export type SystemInstanceListResponse = { ...@@ -77,3 +77,11 @@ export type SystemInstanceListResponse = {
message: string message: string
data?: SystemInstance[] data?: SystemInstance[]
} }
export type SystemInstanceDeleteResponse = {
success: boolean
message: string
data?: {
deleted_count: number
}
}
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "Delete", "Delete": "Delete",
"Delete (": "Delete (", "Delete (": "Delete (",
"Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?", "Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Delete {{count}} stale instance records? Online instances will not be deleted.",
"Delete a runtime request header": "Delete a runtime request header", "Delete a runtime request header": "Delete a runtime request header",
"Delete Account": "Delete Account", "Delete Account": "Delete Account",
"Delete All Disabled": "Delete All Disabled", "Delete All Disabled": "Delete All Disabled",
"Delete All Disabled Channels?": "Delete All Disabled Channels?", "Delete All Disabled Channels?": "Delete All Disabled Channels?",
"Delete all stale": "Delete all stale",
"Delete Auto-Disabled": "Delete Auto-Disabled", "Delete Auto-Disabled": "Delete Auto-Disabled",
"Delete Channel": "Delete Channel", "Delete Channel": "Delete Channel",
"Delete Channels?": "Delete Channels?", "Delete Channels?": "Delete Channels?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "Delete selected API keys", "Delete selected API keys": "Delete selected API keys",
"Delete selected channels": "Delete selected channels", "Delete selected channels": "Delete selected channels",
"Delete selected models": "Delete selected models", "Delete selected models": "Delete selected models",
"Delete stale instance": "Delete stale instance",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.",
"Delete stale instances": "Delete stale instances",
"Deleted": "Deleted", "Deleted": "Deleted",
"Deleted \"{{name}}\"": "Deleted \"{{name}}\"", "Deleted \"{{name}}\"": "Deleted \"{{name}}\"",
"Deleted ({{id}})": "Deleted ({{id}})", "Deleted ({{id}})": "Deleted ({{id}})",
"Deleted {{count}} failed models": "Deleted {{count}} failed models", "Deleted {{count}} failed models": "Deleted {{count}} failed models",
"Deleted {{count}} stale instances": "Deleted {{count}} stale instances",
"Deleted a custom OAuth provider": "Deleted a custom OAuth provider", "Deleted a custom OAuth provider": "Deleted a custom OAuth provider",
"Deleted a deployment": "Deleted a deployment", "Deleted a deployment": "Deleted a deployment",
"Deleted a model": "Deleted a model", "Deleted a model": "Deleted a model",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "Deleted all disabled channels ({{count}})", "Deleted all disabled channels ({{count}})": "Deleted all disabled channels ({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "Deleted channel {{name}} (ID: {{id}})", "Deleted channel {{name}} (ID: {{id}})": "Deleted channel {{name}} (ID: {{id}})",
"Deleted invalid redemption codes": "Deleted invalid redemption codes", "Deleted invalid redemption codes": "Deleted invalid redemption codes",
"Deleted stale instance": "Deleted stale instance",
"Deleted successfully": "Deleted successfully", "Deleted successfully": "Deleted successfully",
"Deleted user {{username}} (ID: {{id}})": "Deleted user {{username}} (ID: {{id}})", "Deleted user {{username}} (ID: {{id}})": "Deleted user {{username}} (ID: {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "Deleting will permanently remove this subscription record (including benefit details). Continue?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Deleting will permanently remove this subscription record (including benefit details). Continue?",
......
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "Supprimer", "Delete": "Supprimer",
"Delete (": "Supprimer (", "Delete (": "Supprimer (",
"Delete {{count}} API key(s)?": "Supprimer {{count}} clé(s) API ?", "Delete {{count}} API key(s)?": "Supprimer {{count}} clé(s) API ?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Supprimer {{count}} enregistrement(s) d'instance expirée ? Les instances en ligne ne seront pas supprimées.",
"Delete a runtime request header": "Supprimer un en-tête de requête à l'exécution", "Delete a runtime request header": "Supprimer un en-tête de requête à l'exécution",
"Delete Account": "Supprimer le compte", "Delete Account": "Supprimer le compte",
"Delete All Disabled": "Supprimer tout ce qui est désactivé", "Delete All Disabled": "Supprimer tout ce qui est désactivé",
"Delete All Disabled Channels?": "Supprimer tous les canaux désactivés ?", "Delete All Disabled Channels?": "Supprimer tous les canaux désactivés ?",
"Delete all stale": "Supprimer toutes les expirées",
"Delete Auto-Disabled": "Supprimer les désactivés automatiquement", "Delete Auto-Disabled": "Supprimer les désactivés automatiquement",
"Delete Channel": "Supprimer le canal", "Delete Channel": "Supprimer le canal",
"Delete Channels?": "Supprimer les canaux ?", "Delete Channels?": "Supprimer les canaux ?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "Supprimer les clés API sélectionnées", "Delete selected API keys": "Supprimer les clés API sélectionnées",
"Delete selected channels": "Supprimer les canaux sélectionnés", "Delete selected channels": "Supprimer les canaux sélectionnés",
"Delete selected models": "Supprimer les modèles sélectionnés", "Delete selected models": "Supprimer les modèles sélectionnés",
"Delete stale instance": "Supprimer l'instance expirée",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Supprimer l'instance expirée \"{{name}}\" ? Si elle a de nouveau signalé son état, elle ne sera pas supprimée.",
"Delete stale instances": "Supprimer les instances expirées",
"Deleted": "Supprimé", "Deleted": "Supprimé",
"Deleted \"{{name}}\"": "\"{{name}}\" supprimé", "Deleted \"{{name}}\"": "\"{{name}}\" supprimé",
"Deleted ({{id}})": "Supprimé ({{id}})", "Deleted ({{id}})": "Supprimé ({{id}})",
"Deleted {{count}} failed models": "{{count}} modèles en échec supprimés", "Deleted {{count}} failed models": "{{count}} modèles en échec supprimés",
"Deleted {{count}} stale instances": "{{count}} instances expirées supprimées",
"Deleted a custom OAuth provider": "Fournisseur OAuth personnalisé supprimé", "Deleted a custom OAuth provider": "Fournisseur OAuth personnalisé supprimé",
"Deleted a deployment": "Déploiement supprimé", "Deleted a deployment": "Déploiement supprimé",
"Deleted a model": "Modèle supprimé", "Deleted a model": "Modèle supprimé",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "Tous les canaux désactivés supprimés ({{count}})", "Deleted all disabled channels ({{count}})": "Tous les canaux désactivés supprimés ({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "Canal {{name}} supprimé (ID : {{id}})", "Deleted channel {{name}} (ID: {{id}})": "Canal {{name}} supprimé (ID : {{id}})",
"Deleted invalid redemption codes": "Codes de réduction invalides supprimés", "Deleted invalid redemption codes": "Codes de réduction invalides supprimés",
"Deleted stale instance": "Instance expirée supprimée",
"Deleted successfully": "Supprimé avec succès", "Deleted successfully": "Supprimé avec succès",
"Deleted user {{username}} (ID: {{id}})": "Utilisateur {{username}} supprimé (ID : {{id}})", "Deleted user {{username}} (ID: {{id}})": "Utilisateur {{username}} supprimé (ID : {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "La suppression supprimera définitivement cet enregistrement d'abonnement (y compris les détails des avantages). Continuer ?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "La suppression supprimera définitivement cet enregistrement d'abonnement (y compris les détails des avantages). Continuer ?",
......
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "削除", "Delete": "削除",
"Delete (": "削除 (", "Delete (": "削除 (",
"Delete {{count}} API key(s)?": "{{count}}個のAPIキーを削除しますか?", "Delete {{count}} API key(s)?": "{{count}}個のAPIキーを削除しますか?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "期限切れインスタンスレコードを {{count}} 件削除しますか?オンラインのインスタンスは削除されません。",
"Delete a runtime request header": "ランタイムリクエストヘッダーを削除", "Delete a runtime request header": "ランタイムリクエストヘッダーを削除",
"Delete Account": "アカウント削除", "Delete Account": "アカウント削除",
"Delete All Disabled": "すべての無効なものを削除", "Delete All Disabled": "すべての無効なものを削除",
"Delete All Disabled Channels?": "すべての無効なチャネルを削除しますか?", "Delete All Disabled Channels?": "すべての無効なチャネルを削除しますか?",
"Delete all stale": "期限切れをすべて削除",
"Delete Auto-Disabled": "自動無効化されたものを削除", "Delete Auto-Disabled": "自動無効化されたものを削除",
"Delete Channel": "チャネルを削除", "Delete Channel": "チャネルを削除",
"Delete Channels?": "チャネルを削除しますか?", "Delete Channels?": "チャネルを削除しますか?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "選択したAPIキーを削除", "Delete selected API keys": "選択したAPIキーを削除",
"Delete selected channels": "選択したチャネルを削除", "Delete selected channels": "選択したチャネルを削除",
"Delete selected models": "選択したモデルを削除", "Delete selected models": "選択したモデルを削除",
"Delete stale instance": "期限切れインスタンスを削除",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "期限切れインスタンス \"{{name}}\" を削除しますか?再度報告されている場合は削除されません。",
"Delete stale instances": "期限切れインスタンスを削除",
"Deleted": "削除済み", "Deleted": "削除済み",
"Deleted \"{{name}}\"": "\"{{name}}\" を削除しました", "Deleted \"{{name}}\"": "\"{{name}}\" を削除しました",
"Deleted ({{id}})": "削除済み ({{id}})", "Deleted ({{id}})": "削除済み ({{id}})",
"Deleted {{count}} failed models": "失敗したモデルを {{count}} 個削除しました", "Deleted {{count}} failed models": "失敗したモデルを {{count}} 個削除しました",
"Deleted {{count}} stale instances": "期限切れインスタンスを {{count}} 件削除しました",
"Deleted a custom OAuth provider": "カスタム OAuth プロバイダーを削除しました", "Deleted a custom OAuth provider": "カスタム OAuth プロバイダーを削除しました",
"Deleted a deployment": "デプロイメントを削除しました", "Deleted a deployment": "デプロイメントを削除しました",
"Deleted a model": "モデルを削除しました", "Deleted a model": "モデルを削除しました",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "無効なチャネルをすべて削除しました({{count}})", "Deleted all disabled channels ({{count}})": "無効なチャネルをすべて削除しました({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "チャネル {{name}} を削除しました(ID: {{id}})", "Deleted channel {{name}} (ID: {{id}})": "チャネル {{name}} を削除しました(ID: {{id}})",
"Deleted invalid redemption codes": "無効な引換コードを削除しました", "Deleted invalid redemption codes": "無効な引換コードを削除しました",
"Deleted stale instance": "期限切れインスタンスを削除しました",
"Deleted successfully": "削除しました", "Deleted successfully": "削除しました",
"Deleted user {{username}} (ID: {{id}})": "ユーザー {{username}} を削除しました(ID: {{id}})", "Deleted user {{username}} (ID: {{id}})": "ユーザー {{username}} を削除しました(ID: {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "削除するとこのサブスクリプション記録(特典詳細を含む)が完全に削除されます。続行しますか?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "削除するとこのサブスクリプション記録(特典詳細を含む)が完全に削除されます。続行しますか?",
......
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "Удалить", "Delete": "Удалить",
"Delete (": "Удалить (", "Delete (": "Удалить (",
"Delete {{count}} API key(s)?": "Удалить {{count}} API-ключ(а/ей)?", "Delete {{count}} API key(s)?": "Удалить {{count}} API-ключ(а/ей)?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Удалить {{count}} записей устаревших экземпляров? Онлайн-экземпляры не будут удалены.",
"Delete a runtime request header": "Удалить заголовок запроса во время выполнения", "Delete a runtime request header": "Удалить заголовок запроса во время выполнения",
"Delete Account": "Удалить аккаунт", "Delete Account": "Удалить аккаунт",
"Delete All Disabled": "Удалить все отключенные", "Delete All Disabled": "Удалить все отключенные",
"Delete All Disabled Channels?": "Удалить все отключенные каналы?", "Delete All Disabled Channels?": "Удалить все отключенные каналы?",
"Delete all stale": "Удалить все устаревшие",
"Delete Auto-Disabled": "Удалить автоматически отключенные", "Delete Auto-Disabled": "Удалить автоматически отключенные",
"Delete Channel": "Удалить канал", "Delete Channel": "Удалить канал",
"Delete Channels?": "Удалить каналы?", "Delete Channels?": "Удалить каналы?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "Удалить выбранные ключи API", "Delete selected API keys": "Удалить выбранные ключи API",
"Delete selected channels": "Удалить выбранные каналы", "Delete selected channels": "Удалить выбранные каналы",
"Delete selected models": "Удалить выбранные модели", "Delete selected models": "Удалить выбранные модели",
"Delete stale instance": "Удалить устаревший экземпляр",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Удалить устаревший экземпляр \"{{name}}\"? Если он снова отправил отчет, он не будет удален.",
"Delete stale instances": "Удалить устаревшие экземпляры",
"Deleted": "Удалён", "Deleted": "Удалён",
"Deleted \"{{name}}\"": "\"{{name}}\" удалено", "Deleted \"{{name}}\"": "\"{{name}}\" удалено",
"Deleted ({{id}})": "Удалён ({{id}})", "Deleted ({{id}})": "Удалён ({{id}})",
"Deleted {{count}} failed models": "Удалено неуспешных моделей: {{count}}", "Deleted {{count}} failed models": "Удалено неуспешных моделей: {{count}}",
"Deleted {{count}} stale instances": "Удалено устаревших экземпляров: {{count}}",
"Deleted a custom OAuth provider": "Удалён пользовательский провайдер OAuth", "Deleted a custom OAuth provider": "Удалён пользовательский провайдер OAuth",
"Deleted a deployment": "Удалено развёртывание", "Deleted a deployment": "Удалено развёртывание",
"Deleted a model": "Удалена модель", "Deleted a model": "Удалена модель",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "Удалены все отключённые каналы ({{count}})", "Deleted all disabled channels ({{count}})": "Удалены все отключённые каналы ({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "Удалён канал {{name}} (ID: {{id}})", "Deleted channel {{name}} (ID: {{id}})": "Удалён канал {{name}} (ID: {{id}})",
"Deleted invalid redemption codes": "Недействительные коды погашения удалены", "Deleted invalid redemption codes": "Недействительные коды погашения удалены",
"Deleted stale instance": "Устаревший экземпляр удален",
"Deleted successfully": "Удалено успешно", "Deleted successfully": "Удалено успешно",
"Deleted user {{username}} (ID: {{id}})": "Удалён пользователь {{username}} (ID: {{id}})", "Deleted user {{username}} (ID: {{id}})": "Удалён пользователь {{username}} (ID: {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "Удаление безвозвратно удалит запись подписки (включая детали льгот). Продолжить?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Удаление безвозвратно удалит запись подписки (включая детали льгот). Продолжить?",
......
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "Xóa", "Delete": "Xóa",
"Delete (": "Xóa (", "Delete (": "Xóa (",
"Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?", "Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Xóa {{count}} bản ghi phiên bản mất kết nối? Các phiên bản đang trực tuyến sẽ không bị xóa.",
"Delete a runtime request header": "Xóa header yêu cầu runtime", "Delete a runtime request header": "Xóa header yêu cầu runtime",
"Delete Account": "Xóa tài khoản", "Delete Account": "Xóa tài khoản",
"Delete All Disabled": "Xóa Tất Cả Đã Tắt", "Delete All Disabled": "Xóa Tất Cả Đã Tắt",
"Delete All Disabled Channels?": "Xóa tất cả kênh đã vô hiệu hóa?", "Delete All Disabled Channels?": "Xóa tất cả kênh đã vô hiệu hóa?",
"Delete all stale": "Xóa tất cả mất kết nối",
"Delete Auto-Disabled": "Xóa Tự động vô hiệu hóa", "Delete Auto-Disabled": "Xóa Tự động vô hiệu hóa",
"Delete Channel": "Xóa Kênh", "Delete Channel": "Xóa Kênh",
"Delete Channels?": "Xóa các kênh?", "Delete Channels?": "Xóa các kênh?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "Xóa các khóa API đã chọn", "Delete selected API keys": "Xóa các khóa API đã chọn",
"Delete selected channels": "Xóa các kênh đã chọn", "Delete selected channels": "Xóa các kênh đã chọn",
"Delete selected models": "Xóa các mô hình đã chọn", "Delete selected models": "Xóa các mô hình đã chọn",
"Delete stale instance": "Xóa phiên bản mất kết nối",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Xóa phiên bản mất kết nối \"{{name}}\"? Nếu phiên bản này đã báo cáo lại, nó sẽ không bị xóa.",
"Delete stale instances": "Xóa các phiên bản mất kết nối",
"Deleted": "Đã xóa", "Deleted": "Đã xóa",
"Deleted \"{{name}}\"": "Đã xóa \"{{name}}\"", "Deleted \"{{name}}\"": "Đã xóa \"{{name}}\"",
"Deleted ({{id}})": "Đã xóa ({{id}})", "Deleted ({{id}})": "Đã xóa ({{id}})",
"Deleted {{count}} failed models": "Đã xóa {{count}} mô hình thất bại", "Deleted {{count}} failed models": "Đã xóa {{count}} mô hình thất bại",
"Deleted {{count}} stale instances": "Đã xóa {{count}} phiên bản mất kết nối",
"Deleted a custom OAuth provider": "Đã xóa nhà cung cấp OAuth tùy chỉnh", "Deleted a custom OAuth provider": "Đã xóa nhà cung cấp OAuth tùy chỉnh",
"Deleted a deployment": "Đã xóa một triển khai", "Deleted a deployment": "Đã xóa một triển khai",
"Deleted a model": "Đã xóa một mô hình", "Deleted a model": "Đã xóa một mô hình",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "Đã xóa tất cả kênh bị vô hiệu hóa ({{count}})", "Deleted all disabled channels ({{count}})": "Đã xóa tất cả kênh bị vô hiệu hóa ({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "Đã xóa kênh {{name}} (ID: {{id}})", "Deleted channel {{name}} (ID: {{id}})": "Đã xóa kênh {{name}} (ID: {{id}})",
"Deleted invalid redemption codes": "Đã xóa các mã đổi thưởng không hợp lệ", "Deleted invalid redemption codes": "Đã xóa các mã đổi thưởng không hợp lệ",
"Deleted stale instance": "Đã xóa phiên bản mất kết nối",
"Deleted successfully": "Xóa thành công", "Deleted successfully": "Xóa thành công",
"Deleted user {{username}} (ID: {{id}})": "Đã xóa người dùng {{username}} (ID: {{id}})", "Deleted user {{username}} (ID: {{id}})": "Đã xóa người dùng {{username}} (ID: {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "Xóa sẽ xóa vĩnh viễn bản ghi đăng ký này (bao gồm chi tiết quyền lợi). Tiếp tục?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Xóa sẽ xóa vĩnh viễn bản ghi đăng ký này (bao gồm chi tiết quyền lợi). Tiếp tục?",
......
...@@ -1260,10 +1260,12 @@ ...@@ -1260,10 +1260,12 @@
"Delete": "删除", "Delete": "删除",
"Delete (": "删除 (", "Delete (": "删除 (",
"Delete {{count}} API key(s)?": "删除 {{count}} 个 API 密钥?", "Delete {{count}} API key(s)?": "删除 {{count}} 个 API 密钥?",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "删除 {{count}} 条失联实例记录?在线实例不会被删除。",
"Delete a runtime request header": "删除运行期请求头", "Delete a runtime request header": "删除运行期请求头",
"Delete Account": "删除账户", "Delete Account": "删除账户",
"Delete All Disabled": "删除所有已禁用", "Delete All Disabled": "删除所有已禁用",
"Delete All Disabled Channels?": "删除所有已禁用的渠道?", "Delete All Disabled Channels?": "删除所有已禁用的渠道?",
"Delete all stale": "删除所有失联",
"Delete Auto-Disabled": "删除自动禁用", "Delete Auto-Disabled": "删除自动禁用",
"Delete Channel": "删除渠道", "Delete Channel": "删除渠道",
"Delete Channels?": "删除渠道?", "Delete Channels?": "删除渠道?",
...@@ -1289,10 +1291,14 @@ ...@@ -1289,10 +1291,14 @@
"Delete selected API keys": "删除选定的 API 密钥", "Delete selected API keys": "删除选定的 API 密钥",
"Delete selected channels": "删除所选渠道", "Delete selected channels": "删除所选渠道",
"Delete selected models": "删除选定的模型", "Delete selected models": "删除选定的模型",
"Delete stale instance": "删除失联实例",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "删除失联实例 \"{{name}}\"?如果它已经重新上报,将不会被删除。",
"Delete stale instances": "删除失联实例",
"Deleted": "已注销", "Deleted": "已注销",
"Deleted \"{{name}}\"": "已删除 \"{{name}}\"", "Deleted \"{{name}}\"": "已删除 \"{{name}}\"",
"Deleted ({{id}})": "已删除({{id}})", "Deleted ({{id}})": "已删除({{id}})",
"Deleted {{count}} failed models": "已删除 {{count}} 个失败模型", "Deleted {{count}} failed models": "已删除 {{count}} 个失败模型",
"Deleted {{count}} stale instances": "已删除 {{count}} 个失联实例",
"Deleted a custom OAuth provider": "删除了一个自定义 OAuth 提供方", "Deleted a custom OAuth provider": "删除了一个自定义 OAuth 提供方",
"Deleted a deployment": "删除了一个部署", "Deleted a deployment": "删除了一个部署",
"Deleted a model": "删除了一个模型", "Deleted a model": "删除了一个模型",
...@@ -1303,6 +1309,7 @@ ...@@ -1303,6 +1309,7 @@
"Deleted all disabled channels ({{count}})": "删除全部禁用渠道({{count}})", "Deleted all disabled channels ({{count}})": "删除全部禁用渠道({{count}})",
"Deleted channel {{name}} (ID: {{id}})": "删除渠道 {{name}}(ID: {{id}})", "Deleted channel {{name}} (ID: {{id}})": "删除渠道 {{name}}(ID: {{id}})",
"Deleted invalid redemption codes": "删除无效兑换码", "Deleted invalid redemption codes": "删除无效兑换码",
"Deleted stale instance": "已删除失联实例",
"Deleted successfully": "删除成功", "Deleted successfully": "删除成功",
"Deleted user {{username}} (ID: {{id}})": "删除用户 {{username}}(ID: {{id}})", "Deleted user {{username}} (ID: {{id}})": "删除用户 {{username}}(ID: {{id}})",
"Deleting will permanently remove this subscription record (including benefit details). Continue?": "删除会彻底移除该订阅记录(含权益明细)。是否继续?", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "删除会彻底移除该订阅记录(含权益明细)。是否继续?",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment