chore(i18n,ui): 全站冗长文案精简 + shadcn 样板汉化 + 一致性修复
- 文案:约 25 组长文案中英同步精简(数据源引导/RBAC 注解/采集提示/删除确认等),三处重复 Ingress 教学收敛 - 汉化 shadcn 样板:分页 Previous/Next/aria、弹窗与抽屉 Close 接入 i18n(common.ui.*) - 一致性:查询授权面板 p-4、仪表盘卡片 p-6、分析卡头 pb-3、部署总数图标配色、 每日统计错误可重试、策略卡/最近部署行 truncate、状态徽章字重、三处原生 checkbox 换 shadcn、Label 浏览器圆角 - 词典:删除无引用的 raw.*/views.* 死键;zh 复数键风格统一(importCount/hits) - 快捷查询提示、Ingress 统计口径与表内说明按反馈删减
This commit is contained in:
1 parent
2da0a2bece
commit
3a8eb521e2
34 files changed
+162
-240
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import i18n from '@/i18n'
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
@@ -70,7 +71,7 @@ function DialogContent({
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{i18n.t('ui.close', { ns: 'common' })}</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from '@/i18n'
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@@ -71,13 +72,13 @@ function PaginationPrevious({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
aria-label={i18n.t('ui.goPrevPage', { ns: 'common' })}
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
<span className="hidden sm:block">{i18n.t('ui.prevPage', { ns: 'common' })}</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -88,12 +89,12 @@ function PaginationNext({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
aria-label={i18n.t('ui.goNextPage', { ns: 'common' })}
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<span className="hidden sm:block">{i18n.t('ui.nextPage', { ns: 'common' })}</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
@@ -111,7 +112,7 @@ function PaginationEllipsis({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
<span className="sr-only">{i18n.t('ui.morePages', { ns: 'common' })}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from '@/i18n'
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
@@ -72,7 +73,7 @@ function SheetContent({
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{i18n.t('ui.close', { ns: 'common' })}</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
sourcePlaceholder: 'Select a data source',
|
||||
query: 'LogQL query *',
|
||||
queryHint:
|
||||
'Common: {app="xxx"} |= "FATAL" / {namespace=~".+"} |~ "(?i)error" / {app="x"} |= "OutOfMemoryError"',
|
||||
'Common: {app="xxx"} |= "FATAL"',
|
||||
channels: 'Channels (multi-select to fan out to multiple alert groups)',
|
||||
noChannels: 'Create channels first under "Settings → Alert Notifications"',
|
||||
channelDisabled: '(disabled)',
|
||||
|
||||
@@ -64,4 +64,13 @@ export default {
|
||||
untagged: '(untagged)',
|
||||
empty: 'No app-dimension data',
|
||||
},
|
||||
}
|
||||
ingress: {
|
||||
title: 'Ingress access analysis',
|
||||
noIngress: 'No ingress-nginx / ingress namespace access logs found on this source',
|
||||
empty: 'No parseable ingress access logs in this time range',
|
||||
loading: 'Loading ingress access logs…',
|
||||
notDetected: 'No ingress-nginx detected',
|
||||
toggleAria: 'Expand / collapse Ingress access analysis',
|
||||
loadFailed: 'Failed to load ingress access logs',
|
||||
},
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
},
|
||||
delete: {
|
||||
warning:
|
||||
'Delete "{{name}}"? Warning: {{dep}} deployments and {{col}} collector policies still reference this cluster; they will stop working once it is deleted.',
|
||||
'Delete "{{name}}"? {{dep}} deployment(s) and {{col}} collector policy(ies) still reference it.',
|
||||
},
|
||||
toast: {
|
||||
deleteFailed: 'Failed to delete cluster',
|
||||
@@ -52,13 +52,13 @@ export default {
|
||||
rbacScope:
|
||||
'Grant permissions only in the <code>log</code> namespace so a leaked Token cannot affect other namespaces; <strong>do not use cluster-admin</strong>.',
|
||||
rbacScriptIntro:
|
||||
'Creates the <code>log</code> namespace, ServiceAccount, namespace-scoped Role and a long-lived Token in one shot (run it once in the cluster, then paste the Token into the field below):',
|
||||
'Run once in the cluster to create the namespace / SA / Role / long-lived Token:',
|
||||
rbacScriptComment1: '# 1) Create in one shot: namespace + SA + namespace-scoped Role + long-lived Token',
|
||||
rbacScriptComment2: '# 2) Extract the long-lived Token (it does not expire) and paste it into the Token field',
|
||||
rbacNamespaceNote:
|
||||
'The Namespace above is created by an administrator; the platform Token itself has no cluster-wide permissions. Only grant the <code>namespaces: create/get</code> ClusterRole if the platform really must create its own namespaces — note that namespace creation cannot be restricted by name, so pre-creating <code>log</code> is safer.',
|
||||
'The token has no cluster-level rights; grant namespaces: create/get only if the platform must create its own namespace.',
|
||||
rbacTokenNote:
|
||||
'The Token is long-lived but bound to a Role in the <code>log</code> namespace only (it can only touch Pods and workloads there), so a leak is limited to <code>log</code>. The server converts it to a kubeconfig and stores it encrypted with <code>ENCRYPTION_KEY</code>; the database keeps only <code>ENC:</code> ciphertext and the API never returns the plaintext. To rotate it, delete and recreate the Secret — the old Token stops working immediately.',
|
||||
'Token is scoped to the log namespace with small blast radius; stored encrypted server-side. Rotate by deleting the Secret and recreating.',
|
||||
contextLabel: 'Context (optional)',
|
||||
contextPlaceholder: 'Leave blank to use the default context',
|
||||
submitAdd: 'Add Cluster',
|
||||
|
||||
@@ -77,7 +77,7 @@ const collectors = {
|
||||
logPath: 'Log Path',
|
||||
logPathHostHint: 'Supports glob wildcards (e.g. /var/log/*.log /var/log/syslog); separate multiple paths with commas',
|
||||
logPathSidecarHint:
|
||||
'Enter the file path your app writes to, e.g. /var/log/app/*.log. Existing log volumes are shared automatically; without one, a shared volume is created only in the dedicated logs directory. Regular paths such as /tmp/test.log are not mounted automatically.',
|
||||
'Paths the app writes to, e.g. /var/log/app/*.log; only existing log volumes or the dedicated logs directory are shared.',
|
||||
labels: 'Custom Labels (JSON, optional)',
|
||||
labelsHint: 'When valid JSON, attached as labels to every log line for filtering in Loki/Grafana; leave empty to skip',
|
||||
submit: 'Create & Deploy',
|
||||
@@ -103,11 +103,11 @@ const collectors = {
|
||||
nsNoMatch: 'No matches',
|
||||
nsLoadFailed: 'Failed to load namespaces',
|
||||
nsHint:
|
||||
'Check "All namespaces" to collect cluster-wide; otherwise only the selected namespaces are collected. Generates the <code>kubernetes_sd_configs.namespaces</code> allowlist for Promtail.',
|
||||
'"All namespaces" collects cluster-wide; otherwise only the checked ones.',
|
||||
appLabelSource: 'app label source',
|
||||
appSourcePlaceholder: 'Empty = default (controller-name fallback chain)',
|
||||
appSourceHint:
|
||||
'By default falls back through <code>app.kubernetes.io/name → app → controller name → pod name</code>. If your cluster has its own convention label (e.g. <code>k8s.kuboard.cn/name</code>), enter it here and it takes the highest priority; the rest still serve as fallback.',
|
||||
'Falls back through <code>app.kubernetes.io/name → app → controller name → pod name</code>; a cluster-specific label (e.g. <code>k8s.kuboard.cn/name</code>) set here takes priority.',
|
||||
appSourcePreset: {
|
||||
default: 'Default: app.kubernetes.io/name → app → controller name → pod name',
|
||||
controllerName: 'controller_name (ReplicaSet name of the Pod, hash stripped = Deployment name)',
|
||||
@@ -118,8 +118,8 @@ const collectors = {
|
||||
appInclude: 'Only collect these apps (optional)',
|
||||
appIncludePlaceholder: 'Empty = no limit; comma-separated, e.g. cex-spot-api, cex-asset-api',
|
||||
appIncludeHint:
|
||||
'When set, Promtail keeps only logs whose app label matches these values (<code>action: keep</code>) — the most direct way to control log volume. Values must match the app resolved by the "app label source" above.',
|
||||
logPathHint: 'The Sidecar requires the app container\'s shared log volume or the dedicated logs directory. Click "Deploy" after saving to apply the new configuration.',
|
||||
'Keeps only logs whose app matches these values (<code>action: keep</code>) — the most direct way to cap log volume.',
|
||||
logPathHint: 'Sidecar needs a shared log volume or the dedicated logs directory; click Deploy after saving.',
|
||||
save: 'Save',
|
||||
saving: 'Saving…',
|
||||
validation: {
|
||||
|
||||
@@ -106,6 +106,14 @@ export default {
|
||||
apply: 'Apply',
|
||||
recent: 'Recently used',
|
||||
},
|
||||
ui: {
|
||||
close: 'Close',
|
||||
prevPage: 'Previous',
|
||||
nextPage: 'Next',
|
||||
goPrevPage: 'Go to previous page',
|
||||
goNextPage: 'Go to next page',
|
||||
morePages: 'More pages',
|
||||
},
|
||||
password: {
|
||||
hide: 'Hide password',
|
||||
show: 'Show password',
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
deleteDockerDesc:
|
||||
'Delete "{{name}}"? The Loki container on the host will be removed as well. This cannot be undone.',
|
||||
deleteRecordDesc:
|
||||
'Delete "{{name}}"? Deleting the record does not uninstall the Loki resources already deployed on the cluster/host. This cannot be undone.',
|
||||
'Deleting "{{name}}" does not uninstall deployed Loki resources; this cannot be undone.',
|
||||
errLoadRefs: 'Failed to load cluster/host/storage data',
|
||||
errDeploy: 'Deployment failed',
|
||||
errStop: 'Failed to stop',
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
ready: 'Loki ready',
|
||||
notReady: 'Loki not ready',
|
||||
runningHint:
|
||||
'Tip: link Loki under "Log Query → Data Sources" to start querying; for cross-cluster/public access, configure an Ingress domain for Loki first.',
|
||||
'Link the Loki source under "Log Query → Data Sources" to query logs.',
|
||||
configure: 'Configure',
|
||||
deploy: 'Deploy',
|
||||
deploying: 'Deploying...',
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
loading: 'Loading…',
|
||||
templateNote: 'Generated by the deployment template',
|
||||
yamlLabel: 'Retention Config YAML',
|
||||
yamlHint: 'Edit limits_config.retention_period, e.g. 90d or 2160h. Only this setting is accepted here.',
|
||||
yamlHint: 'Edit retention_period, e.g. 90d / 2160h; only this setting is accepted.',
|
||||
applyHintRunning:
|
||||
'Save only updates the deployment record; Save & Apply redeploys Loki and queries may be briefly interrupted.',
|
||||
applyHintIdle: 'Takes effect on the next deployment.',
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
},
|
||||
delete: {
|
||||
warning:
|
||||
'Delete "{{name}}"? Warning: {{dep}} deployments and {{col}} collector policies still reference this host; they will stop working once it is deleted.',
|
||||
'Delete "{{name}}"? {{dep}} deployment(s) and {{col}} collector policy(ies) still reference it.',
|
||||
},
|
||||
toast: {
|
||||
deleteFailed: 'Failed to delete host',
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
query: 'Loki Query',
|
||||
access: 'Query Access',
|
||||
sources: 'Data Sources',
|
||||
descQuery: 'Search by log stream and time range; drag the timeline to narrow the range, and click a log line to view its context.',
|
||||
descQuery: 'Search by stream selector and time range; drag the timeline or open line context.',
|
||||
descAccess: 'Reuse log scopes with policy groups and attach multiple users at once.',
|
||||
descSources: 'Configure Loki connections and authentication.',
|
||||
},
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
sourceAria: 'Data source',
|
||||
sourceEmptyOption: 'No data sources available',
|
||||
limitLabel: 'Per page',
|
||||
limitHint: 'Up to 1000 entries per page; paging keeps only the current page. For wide ranges, check the timeline first, then drag to select a peak window.',
|
||||
limitHint: 'Up to 1000 entries per page; paging keeps only the current page.',
|
||||
rangeInvalid: 'Select a valid start and end time',
|
||||
queryFailed: 'Query failed',
|
||||
},
|
||||
@@ -28,14 +28,28 @@ export default {
|
||||
loadFailed: 'Failed to load data sources',
|
||||
missingTitle: 'No Loki data source available',
|
||||
missingBody:
|
||||
'Link a Loki data source first under "Data Sources": for in-cluster Loki, pick "Cluster Service" (Namespace / Service / port, no Ingress needed); for cross-cluster or public queries, expose Loki via an Ingress domain first, then add it with "Direct URL". Queries are unavailable until a data source is linked.',
|
||||
'Link a Loki data source first under "Data Sources": use "Cluster Service" in-cluster, or "Direct URL" via an Ingress domain.',
|
||||
goConfigure: 'Go to data sources',
|
||||
sectionAria: 'Data source management',
|
||||
heading: 'Loki Data Sources',
|
||||
hint: 'Tip: when Loki runs in the cluster, add a "Cluster Service" data source directly (no Ingress needed); for cross-cluster / public queries, expose Loki via an Ingress domain first, then link it with "Direct URL". Without a linked data source, the query page cannot be used.',
|
||||
hint: 'In-cluster Loki works with "Cluster Service"; cross-cluster / public access needs an Ingress domain plus "Direct URL".',
|
||||
managedCluster: 'Cluster {{name}}',
|
||||
managedBadge: 'Platform deployed · managed with the deployment',
|
||||
add: '+ Add manually',
|
||||
countLabel: '{{count}} total',
|
||||
create: 'New source',
|
||||
createTitle: 'New data source',
|
||||
editTitle: 'Edit data source',
|
||||
edit: 'Edit',
|
||||
cancel: 'Cancel',
|
||||
testing: 'Testing…',
|
||||
deleteTitle: 'Delete data source',
|
||||
deleteDesc: 'Deleting "{{name}}" breaks queries and access policies based on it. Delete it?',
|
||||
deleteAria: 'Delete {{name}}',
|
||||
defaultBadge: 'Default',
|
||||
emptyTitle: 'No data sources yet',
|
||||
emptyDesc: 'Add a "Cluster Service" or "Direct URL" source, then query logs on the Loki Query tab.',
|
||||
typeLabel: 'Type',
|
||||
namePlaceholder: 'prod-loki',
|
||||
nameLabel: 'Name',
|
||||
targetUrl: 'Direct URL',
|
||||
targetClusterSvc: 'Cluster Service',
|
||||
@@ -62,17 +76,6 @@ export default {
|
||||
saveFailed: 'Failed to save',
|
||||
remove: 'Delete',
|
||||
},
|
||||
range: {
|
||||
lastMinutes: 'Last {{count}} minutes',
|
||||
lastHours: 'Last {{count}} hours',
|
||||
lastHours_one: 'Last {{count}} hour',
|
||||
custom: 'Custom time',
|
||||
title: 'Select time range',
|
||||
start: 'Start time',
|
||||
end: 'End time',
|
||||
apply: 'Apply',
|
||||
recent: 'Recently used',
|
||||
},
|
||||
editor: {
|
||||
aria: 'LogQL query',
|
||||
valuesOf: 'Values of {{label}}',
|
||||
@@ -85,7 +88,7 @@ export default {
|
||||
templates: {
|
||||
aria: 'Quick query templates',
|
||||
title: 'Quick LogQL builder',
|
||||
hint: 'Pick a namespace, then an app, then a pod; app candidates come only from the selected namespace.',
|
||||
hint: 'Pick a namespace to narrow down; selecting an app / pod runs the query.',
|
||||
namespace: 'Namespace',
|
||||
app: 'App',
|
||||
appWithKey: 'App ({{key}})',
|
||||
@@ -95,12 +98,10 @@ export default {
|
||||
pickNsFirst: 'Pick a namespace first to narrow down',
|
||||
noPod: 'No pods under this app',
|
||||
optional: 'Optional',
|
||||
scopeLogs: 'Query this scope',
|
||||
errorLogs: 'Errors (error)',
|
||||
warnLogs: 'Warnings (warn)',
|
||||
podLogs: 'Specific pod logs',
|
||||
ingressLogs: 'Ingress access logs',
|
||||
scopeHint: 'Pick a namespace, then an app or pod — or choose values in the Label browser',
|
||||
scopeHint: 'Selecting an app / pod runs the query — or pick values in the Label browser',
|
||||
},
|
||||
labels: {
|
||||
browser: 'Label Browser',
|
||||
@@ -115,11 +116,11 @@ export default {
|
||||
loading: 'Loading…',
|
||||
noValues: 'No matching values',
|
||||
useAsValue: 'Use "{{value}}" as value',
|
||||
hint: 'Select namespace first to query all replicas in that Namespace; then app / service_name to narrow to a service (workloads without an app label can rely on namespace alone). Candidates come from logs written to Loki within the current time range; the authorized scope is still enforced server-side.',
|
||||
hint: 'Pick namespace, then app / service_name to narrow down; candidates come from logs in the current range.',
|
||||
},
|
||||
volume: {
|
||||
statsLoading: 'Counting logs across the whole time range; you can keep browsing the log page.',
|
||||
statsError: 'Log volume statistics timed out or failed; the log page still works. Narrow the time range and query again.',
|
||||
statsError: 'Volume stats failed; narrow the range and retry.',
|
||||
histogramLoading: 'Counting…',
|
||||
histogramEmpty: 'No log volume data',
|
||||
timelineAria: 'Log volume timeline',
|
||||
@@ -136,16 +137,10 @@ export default {
|
||||
summary_one: 'Page {{page}} · {{count}} entry',
|
||||
summaryMore: 'Page {{page}} · {{count}} entries · more available',
|
||||
summaryMore_one: 'Page {{page}} · {{count}} entry · more available',
|
||||
viewAria: 'Log display mode',
|
||||
viewRaw: 'Raw',
|
||||
viewTable: 'Table',
|
||||
viewLine: 'Line',
|
||||
viewStream: 'Stream',
|
||||
viewPie: 'Pie',
|
||||
loadingPage: 'Loading log page',
|
||||
loadingQuery: 'Querying Loki',
|
||||
loadingWait: 'Waiting for {{seconds}}s. Wide-range queries may take a while.',
|
||||
pagingHint: 'Logs are paged newest to oldest; charts show the current page, and the top timeline covers the whole query range.',
|
||||
pagingHint: 'Logs page newest-first; the top timeline covers the whole query range.',
|
||||
domainAll: 'All domains',
|
||||
domainFilter: 'Domain filter',
|
||||
domainPlaceholder: 'Type or pick a domain',
|
||||
@@ -165,42 +160,6 @@ export default {
|
||||
idleHint: 'You can also build queries quickly in the Label browser',
|
||||
retry: 'Query again',
|
||||
},
|
||||
views: {
|
||||
noDomain: 'No domain recorded',
|
||||
otherDomain: 'Other domains',
|
||||
unknown: 'Unknown',
|
||||
untagged: 'Untagged',
|
||||
other: 'Other',
|
||||
tableLoaded: '{{count}} log entries loaded on this page.',
|
||||
tableEmpty: 'No logs to display',
|
||||
thTime: 'Time',
|
||||
thNsPod: 'namespace / pod',
|
||||
thLevel: 'Level',
|
||||
thLog: 'Log',
|
||||
lineByLoaded: 'Based on currently loaded logs; load more for additional results.',
|
||||
lineByRange: 'Log volume aggregated over the query time range, from Loki.',
|
||||
lineGroupAria: 'Line chart grouping',
|
||||
lineByDomain: 'By domain',
|
||||
lineByTotal: 'Total',
|
||||
lineEmpty: 'No time-series data for this query',
|
||||
lineSeriesTotal: 'Log count',
|
||||
streamParsed: 'Showing {{count}} parsed entries',
|
||||
streamByLevel: 'Showing {{count}} loaded entries by level',
|
||||
streamSuffix: ', based on currently loaded results.',
|
||||
streamGroupAria: 'Stream chart grouping',
|
||||
streamByStatus: 'By HTTP status class',
|
||||
streamEmpty: 'No log streams to display',
|
||||
pieByAccess: 'Grouping {{count}} parsed entries by access fields',
|
||||
pieByLabel: 'Grouping {{count}} loaded entries by label',
|
||||
pieGroupAria: 'Pie chart grouping',
|
||||
pieDomain: 'Domain (request Host)',
|
||||
pieStatus: 'HTTP status',
|
||||
pieMethod: 'Method',
|
||||
piePath: 'Path',
|
||||
pieUpstream: 'Upstream service',
|
||||
pieLevel: 'Level',
|
||||
pieEmpty: 'No label distribution to display',
|
||||
},
|
||||
ingress: {
|
||||
modesAria: 'Ingress statistics mode',
|
||||
modeTraffic: 'Endpoints PV / UV',
|
||||
@@ -208,7 +167,7 @@ export default {
|
||||
modeSlow: 'Top 10 slow requests',
|
||||
modeDetail: 'Request details',
|
||||
summary:
|
||||
'Based on the {{loaded}} entries loaded on this page, of which {{parsed}} are ingress-nginx access logs. Statistics are recomputed after paging; UV is deduplicated by non-empty client IP.',
|
||||
'Based on {{loaded}} loaded entries',
|
||||
statPv: 'PV',
|
||||
statUv: 'UV (IP)',
|
||||
statErrors: '4xx / 5xx',
|
||||
@@ -219,9 +178,9 @@ export default {
|
||||
error4xx: '4xx only',
|
||||
slowThreshold: 'Slow request threshold',
|
||||
seconds: '{{count}}s',
|
||||
slowHint: 'Sorted by request_time from the access log; 408 / 499 / 504 are only timeout hints and do not by themselves prove a backend timeout.',
|
||||
slowHint: 'Sorted by request_time; 408 / 499 / 504 are timeout hints.',
|
||||
slowMissing: '{{count}} entries lack a valid duration.',
|
||||
trafficHint: 'Success rate = 2xx / 3xx requests ÷ PV; query strings are stripped from paths.',
|
||||
trafficHint: 'Success rate = 2xx / 3xx ÷ PV',
|
||||
thHost: 'Request Host',
|
||||
thPath: 'Path',
|
||||
thSuccess: 'Success rate',
|
||||
@@ -257,7 +216,7 @@ export default {
|
||||
loadFailed: 'Failed to load context',
|
||||
},
|
||||
access: {
|
||||
labelsUnlimited: 'Unlimited · all namespaces',
|
||||
labelsUnlimited: 'Unrestricted · all namespaces',
|
||||
loadFailed: 'Failed to load query policies',
|
||||
validate: 'Fill in the policy group name, choose a data source, and select "Anyone" or at least one user',
|
||||
saveFailed: 'Failed to save policy group',
|
||||
@@ -309,19 +268,4 @@ export default {
|
||||
loading: 'Loading…',
|
||||
noMatch: 'No matches',
|
||||
},
|
||||
raw: {
|
||||
searching: 'Querying…',
|
||||
count: '{{count}} logs',
|
||||
count_one: '{{count}} log',
|
||||
dedupSuffix: ' (deduplicated from {{count}})',
|
||||
dedup: 'Dedup',
|
||||
empty: 'No matching logs',
|
||||
original: 'Raw log',
|
||||
labelsTitle: 'Labels (click to filter)',
|
||||
filterValue: 'Filter this value',
|
||||
excludeValue: 'Exclude this value',
|
||||
fields: 'Extracted fields',
|
||||
loadMore: 'Load more',
|
||||
loadingMore: 'Loading…',
|
||||
},
|
||||
}
|
||||
@@ -45,9 +45,9 @@ export default {
|
||||
cardTitle: 'Alert Channels — Feishu (Lark)',
|
||||
create: 'New channel',
|
||||
// Empty state is assembled as "prefix + link + suffix" (link points to the Alerts page)
|
||||
emptyBefore: 'No channels yet — create one per alert group (e.g. "App alerts", "Nginx alerts"), then link rules to it on the ',
|
||||
emptyBefore: 'No channels yet — create one per alert group, ',
|
||||
emptyLink: 'Alerts',
|
||||
emptyAfter: ' page',
|
||||
emptyAfter: 'then link rules to it on the alerts page.',
|
||||
webhookConfigured: 'Webhook configured',
|
||||
webhookMissing: 'Not configured',
|
||||
secretConfigured: ' · Signing secret configured',
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
headerHint: 'Enables LDAP sign-in and bulk user import',
|
||||
errLoad: 'Failed to load LDAP configuration',
|
||||
disabledHint:
|
||||
'LDAP is disabled — turn it on and fill in the directory details (server / Bind DN / Base DN). After saving, "Import from LDAP" appears on the Users page.',
|
||||
'LDAP disabled — enable it and fill in the directory info to show "Import from LDAP" on the Users page.',
|
||||
hostLabel: 'Server address *',
|
||||
portLabel: 'Port',
|
||||
ldapsLabel: 'LDAPS',
|
||||
@@ -133,7 +133,7 @@ export default {
|
||||
timezone: 'Timezone',
|
||||
timezoneAria: 'Theme schedule timezone',
|
||||
browser: 'Follow browser (local)',
|
||||
autoHint: 'Light from {{start}}:00 to {{end}}:00 in the selected timezone, dark otherwise; the sun/moon toggle overrides anytime.',
|
||||
autoHint: 'Light from {{start}}:00–{{end}}:00 in the chosen timezone, dark otherwise; the top bar toggle always works.',
|
||||
},
|
||||
prefsCard: {
|
||||
title: 'Appearance & Language',
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
deleteWarning_one:
|
||||
'Delete "{{name}}"? Warning: {{count}} deployment still uses this storage; its object storage config will stop working.',
|
||||
deleteWarning_other:
|
||||
'Delete "{{name}}"? Warning: {{count}} deployments still use this storage; their object storage configs will stop working.',
|
||||
'Delete "{{name}}"? {{count}} deployment(s) still use it; their object storage will break.',
|
||||
errDelete: 'Failed to delete the storage config',
|
||||
errSetDefault: 'Failed to set the default storage',
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
clear: 'Clear selection',
|
||||
selectAll: 'Select all',
|
||||
confirmTitle: 'Confirm Bulk Deletion',
|
||||
confirmDesc: 'Delete {{count}} selected users? Protected accounts (your own / the last admin) will be skipped. This cannot be undone.',
|
||||
confirmDesc: 'Delete {{count}} selected users? Protected accounts are skipped; this cannot be undone.',
|
||||
skippedWarn: '{{count}} skipped: {{reasons}}',
|
||||
failed: 'Bulk delete failed',
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ const wizard = {
|
||||
ready: 'Ready',
|
||||
connected: 'Connected: {{names}}',
|
||||
nameSep: ', ',
|
||||
k8sHint: 'Connect a cluster via kubeconfig or ServiceAccount token; connectivity is verified automatically on creation',
|
||||
k8sHint: 'Connect via kubeconfig or Token; connectivity is verified on create.',
|
||||
addCluster: 'Add Cluster',
|
||||
hostHint: 'Connect a host via SSH key or password (used for deployment and later collector installation)',
|
||||
addHost: 'Add Host',
|
||||
@@ -44,7 +44,7 @@ const wizard = {
|
||||
manage: 'Manage Storage',
|
||||
add: 'Add Storage',
|
||||
configure: 'Configure Storage',
|
||||
hint: 'Object storage (MinIO / RustFS / S3 / OSS) is recommended for more reliable replication and retention. Use "Test Connection" when adding to verify credentials and auto-create missing buckets; skip to use local disk.',
|
||||
hint: 'Object storage (MinIO / RustFS / S3 / OSS) recommended; skip to use local disk.',
|
||||
},
|
||||
deploy: {
|
||||
meta: '{{mode}} · Namespace {{namespace}} · {{endpoint}}',
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
sent: '已发送',
|
||||
cooldown: '冷却跳过',
|
||||
failed: '失败',
|
||||
hits_other: '{{count}} 条命中',
|
||||
hits: '{{count}} 条命中',
|
||||
},
|
||||
tpl: {
|
||||
nsError: {
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
sourcePlaceholder: '选择数据源',
|
||||
query: 'LogQL 查询 *',
|
||||
queryHint:
|
||||
'常用:{app="xxx"} |= "FATAL" / {namespace=~".+"} |~ "(?i)error" / {app="x"} |= "OutOfMemoryError"',
|
||||
'常用:{app="xxx"} |= "FATAL"',
|
||||
channels: '通知通道(可多选,扇出到多个告警群)',
|
||||
noChannels: '请先在「系统设置 → 告警通知」创建通道',
|
||||
channelDisabled: '(禁用)',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 日志分析页(admin)— 经 i18n/domains.ts 注册,页面入口 import '@/i18n/domains'
|
||||
export default {
|
||||
title: '日志分析',
|
||||
subtitle: '按应用与命名空间统计日志量、错误量与趋势,定位日志大户',
|
||||
adminOnly: {
|
||||
title: '仅管理员可访问',
|
||||
desc: '日志分析面向管理员提供全量日志统计,请联系管理员开通。',
|
||||
@@ -24,7 +23,6 @@ export default {
|
||||
},
|
||||
trend: {
|
||||
title: '日志量趋势',
|
||||
subtitle: '总量 / 错误 / 告警 按时间分桶',
|
||||
seriesTotal: '总量',
|
||||
seriesError: '错误',
|
||||
seriesWarn: '告警',
|
||||
@@ -32,25 +30,21 @@ export default {
|
||||
},
|
||||
stream: {
|
||||
title: '应用日志量流图',
|
||||
subtitle: '日志量 Top {{count}} 应用随时间的堆叠分布',
|
||||
empty: '没有应用维度的数据',
|
||||
},
|
||||
pie: {
|
||||
title: '命名空间分布',
|
||||
subtitle: '按命名空间统计的日志量占比',
|
||||
empty: '没有命名空间维度的数据',
|
||||
other: '其他',
|
||||
},
|
||||
daily: {
|
||||
fixedBadge: '固定近 7 天',
|
||||
title: '应用每日日志量',
|
||||
subtitle: '固定统计近 7 个自然日的 Top 应用分布,不受上方时间范围影响',
|
||||
empty: '近 7 天没有应用维度的数据',
|
||||
},
|
||||
|
||||
table: {
|
||||
title: '应用日志量排行',
|
||||
subtitle: '按日志量排序的 Top 10 应用,点击「查看日志」跳转查询页',
|
||||
rank: '#',
|
||||
app: '应用',
|
||||
appWithKey: '应用({{key}})',
|
||||
@@ -64,4 +58,13 @@ export default {
|
||||
untagged: '(未打标签)',
|
||||
empty: '没有应用维度的数据',
|
||||
},
|
||||
}
|
||||
ingress: {
|
||||
title: 'Ingress 访问分析',
|
||||
noIngress: '当前数据源未发现 ingress-nginx / ingress 命名空间的访问日志',
|
||||
empty: '该时间范围内没有可解析的 ingress 访问日志',
|
||||
loading: '加载 ingress 访问日志中…',
|
||||
notDetected: '未检测到 ingress-nginx',
|
||||
toggleAria: '展开 / 折叠 Ingress 访问分析',
|
||||
loadFailed: '加载 ingress 访问日志失败',
|
||||
},
|
||||
}
|
||||
@@ -51,13 +51,13 @@ export default {
|
||||
rbacScope:
|
||||
'只授予 <code>log</code> 命名空间权限,Token 泄露也不会影响其他命名空间;<strong>不要使用 cluster-admin</strong>。',
|
||||
rbacScriptIntro:
|
||||
'一次性创建 <code>log</code> 命名空间、ServiceAccount、命名空间级 Role 和长期 Token(在集群里执行一次,Token 粘贴到下方输入框):',
|
||||
'在集群执行一次,创建命名空间 / SA / Role / 长期 Token:',
|
||||
rbacScriptComment1: '# 1) 一次创建:命名空间 + SA + 命名空间级 Role + 长期 Token',
|
||||
rbacScriptComment2: '# 2) 取出长期 Token(不自动过期),粘贴到 Token 输入框',
|
||||
rbacNamespaceNote:
|
||||
'上面的 Namespace 由管理员创建,平台 Token 本身没有任何集群级权限。只有确实需要平台自建命名空间时,才额外授予 <code>namespaces: create/get</code> 的 ClusterRole;注意 namespace 的 create 无法按名称限制,所以预建 <code>log</code> 更安全。',
|
||||
'Token 无集群级权限;仅当需要平台自建命名空间时才额外授予 namespaces: create/get。',
|
||||
rbacTokenNote:
|
||||
'Token 长期有效,但只绑定 <code>log</code> 命名空间的 Role(仅能操作该命名空间的 Pod / 工作负载),泄露影响面仅限于 <code>log</code>;服务端会转成 kubeconfig 并用 <code>ENCRYPTION_KEY</code> 加密存储,数据库里只有 <code>ENC:</code> 密文,接口不返回明文。轮换时删除该 Secret 重建即可,旧 Token 立即失效。',
|
||||
'Token 仅限 log 命名空间,泄露影响面小;服务端加密存储。轮换:删除该 Secret 重建。',
|
||||
contextLabel: '上下文(可选)',
|
||||
contextPlaceholder: '留空则使用默认上下文',
|
||||
submitAdd: '添加集群',
|
||||
|
||||
@@ -80,7 +80,7 @@ const collectors = {
|
||||
logPath: '采集日志路径',
|
||||
logPathHostHint: '支持 glob 通配(如 /var/log/*.log /var/log/syslog);多个路径用逗号分隔',
|
||||
logPathSidecarHint:
|
||||
'填写业务写入的文件路径,如 /var/log/app/*.log。已有日志卷会自动共享;无日志卷时仅在专用 logs 目录创建共享卷。/tmp/test.log 等普通目录不会自动挂载。',
|
||||
'填业务写入的路径,如 /var/log/app/*.log;仅共享已有日志卷或专用 logs 目录。',
|
||||
labels: '自定义标签(JSON,可选)',
|
||||
labelsHint: '合法 JSON 时将附加到每条日志的标签,便于在 Loki/Grafana 中筛选;留空则不加',
|
||||
submit: '创建并部署',
|
||||
@@ -107,11 +107,11 @@ const collectors = {
|
||||
nsLoadFailed: '加载命名空间失败',
|
||||
// nsHint/appSourceHint/appIncludeHint 含行内 <code>,渲染处用 Trans 组件
|
||||
nsHint:
|
||||
'勾「所有命名空间」= 采集全集群;否则只采集勾选的命名空间。生成 Promtail 的 <code>kubernetes_sd_configs.namespaces</code> 白名单。',
|
||||
'勾「所有命名空间」= 采集全集群;否则只采勾选项。',
|
||||
appLabelSource: 'app 标签取值来源',
|
||||
appSourcePlaceholder: '留空 = 默认(controller 名回退链)',
|
||||
appSourceHint:
|
||||
'默认按 <code>app.kubernetes.io/name → app → controller 名 → pod 名</code> 四级回退。若集群有自己的私有规范标签(如 <code>k8s.kuboard.cn/name</code>),填在这里会作为最高优先级,其余仍兜底。',
|
||||
'默认按 <code>app.kubernetes.io/name → app → controller 名 → pod 名</code> 回退;集群自定义标签(如 <code>k8s.kuboard.cn/name</code>)填此处优先。',
|
||||
appSourcePreset: {
|
||||
default: '默认:app.kubernetes.io/name → app → controller 名 → pod 名',
|
||||
controllerName: 'controller_name(Pod 的 ReplicaSet 名,去 hash = Deployment 名)',
|
||||
@@ -122,8 +122,8 @@ const collectors = {
|
||||
appInclude: '只采集这些 app(可选)',
|
||||
appIncludePlaceholder: '留空 = 不限;多个用逗号分隔,如 cex-spot-api, cex-asset-api',
|
||||
appIncludeHint:
|
||||
'填了之后 Promtail 只保留 app 命中这些值的日志(<code>action: keep</code>),是控制日志量最直接的手段。值需与上面「app 标签取值来源」解析出的 app 一致。',
|
||||
logPathHint: 'Sidecar 需使用业务容器的共享日志卷,或专用 logs 目录。保存后点击「部署」应用新配置。',
|
||||
'只保留 app 命中这些值的日志(<code>action: keep</code>),是控制日志量最直接的手段。',
|
||||
logPathHint: 'Sidecar 需共享日志卷或专用 logs 目录;保存后点「部署」生效。',
|
||||
save: '保存',
|
||||
saving: '保存中…',
|
||||
validation: {
|
||||
|
||||
@@ -105,6 +105,14 @@ export default {
|
||||
apply: '应用时间',
|
||||
recent: '最近使用',
|
||||
},
|
||||
ui: {
|
||||
close: '关闭',
|
||||
prevPage: '上一页',
|
||||
nextPage: '下一页',
|
||||
goPrevPage: '前往上一页',
|
||||
goNextPage: '前往下一页',
|
||||
morePages: '更多页码',
|
||||
},
|
||||
password: {
|
||||
hide: '隐藏密码',
|
||||
show: '显示密码',
|
||||
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
// 删除确认:host-docker 连容器一起删,其余模式仅删记录不卸载资源
|
||||
deleteDockerDesc: '确定要删除「{{name}}」吗?将同时移除主机上的 Loki 容器,此操作不可恢复。',
|
||||
deleteRecordDesc:
|
||||
'确定要删除「{{name}}」吗?删除记录不会自动卸载集群/主机上已部署的 Loki 资源,此操作不可恢复。',
|
||||
'删除「{{name}}」不会卸载已部署的 Loki 资源,此操作不可恢复。',
|
||||
errLoadRefs: '加载集群/主机/存储数据失败',
|
||||
errDeploy: '部署失败',
|
||||
errStop: '停止失败',
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
ready: 'Loki 就绪',
|
||||
notReady: 'Loki 未就绪',
|
||||
runningHint:
|
||||
'提示:到「日志查询 → 数据源」关联 Loki 后即可查询;跨集群/公网访问需先为 Loki 配置 Ingress 域名。',
|
||||
'到「日志查询 → 数据源」关联 Loki 后即可查询。',
|
||||
configure: '配置',
|
||||
deploy: '部署',
|
||||
deploying: '部署中...',
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
loading: '读取中…',
|
||||
templateNote: '由部署模板生成',
|
||||
yamlLabel: '保留配置 YAML',
|
||||
yamlHint: '编辑 limits_config.retention_period,例如 90d 或 2160h。此处只接受这一项配置。',
|
||||
yamlHint: '编辑 retention_period,如 90d / 2160h;仅支持这一项。',
|
||||
applyHintRunning: '仅保存会更新部署记录;保存并应用会重新部署 Loki,查询可能短暂中断。',
|
||||
applyHintIdle: '保存后在下次部署时生效。',
|
||||
cancel: '取消',
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
},
|
||||
delete: {
|
||||
warning:
|
||||
'确定要删除「{{name}}」吗?警告:仍有 {{dep}} 个部署、{{col}} 个采集策略引用该主机,删除后它们将无法工作。',
|
||||
'确定删除「{{name}}」吗?仍有 {{dep}} 个部署、{{col}} 个采集策略在引用。',
|
||||
},
|
||||
toast: {
|
||||
deleteFailed: '删除主机失败',
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
query: 'Loki 查询',
|
||||
access: '查询授权',
|
||||
sources: '数据源管理',
|
||||
descQuery: '按日志流和时间范围检索;拖动时间线缩小范围,点日志查看前后文。',
|
||||
descQuery: '按日志流与时间范围检索,可拖时间线、看前后文。',
|
||||
descAccess: '用策略组复用日志范围,并一次关联多个用户。',
|
||||
descSources: '配置 Loki 连接与认证方式。',
|
||||
},
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
sourceAria: '数据源',
|
||||
sourceEmptyOption: '暂无可用数据源',
|
||||
limitLabel: '每页条数',
|
||||
limitHint: '每页最多 1000 条;翻页只保留当前页。大范围先看时间线,再拖选高峰时段。',
|
||||
limitHint: '每页最多 1000 条,翻页仅保留当前页。',
|
||||
rangeInvalid: '请选择有效的起止时间',
|
||||
queryFailed: '查询失败',
|
||||
},
|
||||
@@ -27,14 +27,28 @@ export default {
|
||||
loadFailed: '加载数据源失败',
|
||||
missingTitle: '还没有可用的 Loki 数据源',
|
||||
missingBody:
|
||||
'请先到「数据源管理」关联 Loki:集群内可直接选「集群服务」(命名空间 / Service / 端口,无需 Ingress);跨集群或公网查询,请先给 Loki 配置 Ingress 域名,再用「URL 直连」填写该域名。未关联数据源无法查询。',
|
||||
'请先到「数据源管理」关联 Loki:集群内选「集群服务」,跨集群 / 公网用「URL 直连」。',
|
||||
goConfigure: '去配置数据源',
|
||||
sectionAria: '数据源管理',
|
||||
heading: 'Loki 数据源',
|
||||
hint: '提示:Loki 部署在集群内时,可直接添加「集群服务」数据源(无需 Ingress);跨集群 / 公网查询需先为 Loki 配置 Ingress 域名,再用「URL 直连」关联。未关联数据源时,查询页无法使用。',
|
||||
hint: '集群内 Loki 选「集群服务」即可;跨集群 / 公网需先配 Ingress 域名,再用「URL 直连」。',
|
||||
managedCluster: '集群 {{name}}',
|
||||
managedBadge: '平台部署 · 随部署管理',
|
||||
add: '+ 手动添加',
|
||||
countLabel: '共 {{count}} 个',
|
||||
create: '新建数据源',
|
||||
createTitle: '新建数据源',
|
||||
editTitle: '编辑数据源',
|
||||
edit: '编辑',
|
||||
cancel: '取消',
|
||||
testing: '测试中…',
|
||||
deleteTitle: '删除数据源',
|
||||
deleteDesc: '删除「{{name}}」后,基于它的查询与授权策略将失效,确定删除吗?',
|
||||
deleteAria: '删除 {{name}}',
|
||||
defaultBadge: '默认',
|
||||
emptyTitle: '还没有数据源',
|
||||
emptyDesc: '新建「集群服务」或「URL 直连」数据源后,即可在查询页检索日志。',
|
||||
typeLabel: '类型',
|
||||
namePlaceholder: 'prod-loki',
|
||||
nameLabel: '名称',
|
||||
targetUrl: 'URL 直连',
|
||||
targetClusterSvc: '集群服务',
|
||||
@@ -61,16 +75,6 @@ export default {
|
||||
saveFailed: '保存失败',
|
||||
remove: '删除',
|
||||
},
|
||||
range: {
|
||||
lastMinutes: '最近 {{count}} 分钟',
|
||||
lastHours: '最近 {{count}} 小时',
|
||||
custom: '自定义时间',
|
||||
title: '选择时间范围',
|
||||
start: '开始时间',
|
||||
end: '结束时间',
|
||||
apply: '应用时间',
|
||||
recent: '最近使用',
|
||||
},
|
||||
editor: {
|
||||
aria: 'LogQL 查询语句',
|
||||
valuesOf: '{{label}} 的值',
|
||||
@@ -83,7 +87,7 @@ export default {
|
||||
templates: {
|
||||
aria: '快捷查询模板',
|
||||
title: '快捷生成 LogQL',
|
||||
hint: '先选 namespace,再选 app,最后选副本 pod;app 候选只来自所选命名空间。',
|
||||
hint: '先选 namespace 缩小范围;选中 app / pod 自动查询。',
|
||||
namespace: '命名空间',
|
||||
app: 'App',
|
||||
appWithKey: 'App({{key}})',
|
||||
@@ -93,12 +97,10 @@ export default {
|
||||
pickNsFirst: '先选命名空间可缩小范围',
|
||||
noPod: '该 app 下无副本',
|
||||
optional: '可选',
|
||||
scopeLogs: '查询该范围日志',
|
||||
errorLogs: '错误 error',
|
||||
warnLogs: '告警 warn',
|
||||
podLogs: '指定副本日志',
|
||||
ingressLogs: 'Ingress 访问日志',
|
||||
scopeHint: '先选 namespace,再选 app 或 pod,也可在 Label 浏览器选值',
|
||||
scopeHint: '选 app / pod 即自动查询;也可在 Label 浏览器选值',
|
||||
},
|
||||
labels: {
|
||||
browser: 'Label 浏览器',
|
||||
@@ -113,11 +115,11 @@ export default {
|
||||
loading: '加载中…',
|
||||
noValues: '没有匹配值',
|
||||
useAsValue: '使用“{{value}}”作为值',
|
||||
hint: '先选 namespace 可查该命名空间全部副本;再选 app / service_name 可缩小到某个服务(无 app 标签的工作负载用 namespace 即可)。候选来自当前时间范围内已写入 Loki 的日志;授权范围仍由服务端限制。',
|
||||
hint: '先选 namespace,再选 app / service_name 逐步缩小;候选为当前时间范围内的日志。',
|
||||
},
|
||||
volume: {
|
||||
statsLoading: '正在统计整个时间范围的日志量,日志页可继续浏览。',
|
||||
statsError: '日志量统计超时或失败;日志页仍可使用。可缩小时间范围后重新查询。',
|
||||
statsError: '日志量统计失败;可缩小时间范围后重试。',
|
||||
histogramLoading: '统计中…',
|
||||
histogramEmpty: '暂无日志量数据',
|
||||
timelineAria: '日志量时间线',
|
||||
@@ -131,16 +133,10 @@ export default {
|
||||
title: '日志结果',
|
||||
summary: '第 {{page}} 页 · 本页 {{count}} 条',
|
||||
summaryMore: '第 {{page}} 页 · 本页 {{count}} 条 · 还有更多',
|
||||
viewAria: '日志展示方式',
|
||||
viewRaw: '原始日志',
|
||||
viewTable: '表格',
|
||||
viewLine: '折线图',
|
||||
viewStream: '流图',
|
||||
viewPie: '饼图',
|
||||
loadingPage: '正在加载日志页',
|
||||
loadingQuery: '正在查询 Loki',
|
||||
loadingWait: '已等待 {{seconds}} 秒。大范围查询可能需要较长时间。',
|
||||
pagingHint: '日志按时间从新到旧翻页;图表展示当前页,顶部时间线展示整个查询范围。',
|
||||
pagingHint: '日志按时间从新到旧翻页;顶部时间线为整个查询范围。',
|
||||
domainAll: '全部域名',
|
||||
domainFilter: '域名筛选',
|
||||
domainPlaceholder: '输入或选择域名',
|
||||
@@ -160,42 +156,6 @@ export default {
|
||||
idleHint: '也可以使用 Label 浏览器快速构建查询',
|
||||
retry: '重新查询',
|
||||
},
|
||||
views: {
|
||||
noDomain: '未记录域名',
|
||||
otherDomain: '其他域名',
|
||||
unknown: '未知',
|
||||
untagged: '未标记',
|
||||
other: '其他',
|
||||
tableLoaded: '当前页已加载 {{count}} 条日志。',
|
||||
tableEmpty: '暂无可展示的日志',
|
||||
thTime: '时间',
|
||||
thNsPod: 'namespace / pod',
|
||||
thLevel: '级别',
|
||||
thLog: '日志',
|
||||
lineByLoaded: '按当前已加载日志统计;更多结果请继续加载。',
|
||||
lineByRange: '按查询时间范围统计的日志量,来自 Loki 时间聚合。',
|
||||
lineGroupAria: '折线图分组字段',
|
||||
lineByDomain: '按域名',
|
||||
lineByTotal: '总量',
|
||||
lineEmpty: '当前查询没有可用的时间统计',
|
||||
lineSeriesTotal: '日志条数',
|
||||
streamParsed: '展示 {{count}} 条已解析日志',
|
||||
streamByLevel: '按级别展示当前已加载的 {{count}} 条日志',
|
||||
streamSuffix: ',基于当前已加载结果。',
|
||||
streamGroupAria: '流图分组字段',
|
||||
streamByStatus: '按 HTTP 状态段',
|
||||
streamEmpty: '暂无可展示的日志流',
|
||||
pieByAccess: '按访问字段统计已解析的 {{count}} 条日志',
|
||||
pieByLabel: '按标签统计当前已加载的 {{count}} 条日志',
|
||||
pieGroupAria: '饼图分组字段',
|
||||
pieDomain: '域名(请求 Host)',
|
||||
pieStatus: 'HTTP 状态码',
|
||||
pieMethod: '请求方法',
|
||||
piePath: '请求路径',
|
||||
pieUpstream: '上游服务',
|
||||
pieLevel: '级别',
|
||||
pieEmpty: '暂无可展示的标签分布',
|
||||
},
|
||||
ingress: {
|
||||
modesAria: 'Ingress 统计方式',
|
||||
modeTraffic: '接口 PV / UV',
|
||||
@@ -203,7 +163,7 @@ export default {
|
||||
modeSlow: '慢请求 Top 10',
|
||||
modeDetail: '请求明细',
|
||||
summary:
|
||||
'仅统计当前页已加载的 {{loaded}} 条日志,其中 {{parsed}} 条为 ingress-nginx 访问日志。翻页后重新统计;UV 按非空客户端 IP 去重。',
|
||||
'基于已加载的 {{loaded}} 条日志统计',
|
||||
statPv: 'PV',
|
||||
statUv: 'UV(IP)',
|
||||
statErrors: '4xx / 5xx',
|
||||
@@ -214,9 +174,9 @@ export default {
|
||||
error4xx: '只看 4xx',
|
||||
slowThreshold: '慢请求阈值',
|
||||
seconds: '{{count}} 秒',
|
||||
slowHint: '依据 access log 的 request_time 排序;408 / 499 / 504 仅作超时线索,不能单凭状态码断定后端超时。',
|
||||
slowHint: '按 request_time 排序;408 / 499 / 504 为超时线索。',
|
||||
slowMissing: '{{count}} 条缺少有效耗时。',
|
||||
trafficHint: '访问成功率按 2xx / 3xx 请求数 ÷ PV 计算;路径已去掉查询参数。',
|
||||
trafficHint: '成功率 = 2xx / 3xx ÷ PV',
|
||||
thHost: '请求 Host',
|
||||
thPath: '接口路径',
|
||||
thSuccess: '成功率',
|
||||
@@ -301,18 +261,4 @@ export default {
|
||||
loading: '加载中…',
|
||||
noMatch: '无匹配值',
|
||||
},
|
||||
raw: {
|
||||
searching: '查询中…',
|
||||
count: '{{count}} 条',
|
||||
dedupSuffix: '(去重后,原 {{count}} 条)',
|
||||
dedup: '去重',
|
||||
empty: '没有匹配的日志',
|
||||
original: '日志原文',
|
||||
labelsTitle: '标签(点击过滤)',
|
||||
filterValue: '过滤此值',
|
||||
excludeValue: '排除此值',
|
||||
fields: '提取字段',
|
||||
loadMore: '加载更多',
|
||||
loadingMore: '加载中…',
|
||||
},
|
||||
}
|
||||
@@ -45,9 +45,9 @@ export default {
|
||||
cardTitle: '告警通知通道(飞书/Lark)',
|
||||
create: '新建通道',
|
||||
// 空态文案按「前段 + 链接 + 后段」拼接(链接指向日志告警页)
|
||||
emptyBefore: '尚无通知通道 —— 每个告警群创建一个通道(如「应用告警群」「Nginx 告警群」),在',
|
||||
emptyBefore: '尚无通知通道 —— 每个告警群建一个通道,',
|
||||
emptyLink: '日志告警',
|
||||
emptyAfter: '页把规则关联到对应通道',
|
||||
emptyAfter: '在告警规则页把规则关联到对应通道。',
|
||||
webhookConfigured: 'Webhook 已配置',
|
||||
webhookMissing: '未配置',
|
||||
secretConfigured: ' · 签名已配置',
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
headerHint: '启用后支持 LDAP 登录与批量导入',
|
||||
errLoad: '加载 LDAP 配置失败',
|
||||
disabledHint:
|
||||
'LDAP 未启用 —— 打开开关填写目录服务信息(服务器 / Bind DN / Base DN),保存后用户管理页将出现「从 LDAP 导入」',
|
||||
'LDAP 未启用 —— 开启并填写目录服务信息后,用户管理页将出现「从 LDAP 导入」。',
|
||||
hostLabel: '服务器地址 *',
|
||||
portLabel: '端口',
|
||||
ldapsLabel: 'LDAPS',
|
||||
@@ -133,7 +133,7 @@ export default {
|
||||
timezone: '判定时区',
|
||||
timezoneAria: '主题定时切换时区',
|
||||
browser: '跟随浏览器(本地)',
|
||||
autoHint: '按所选时区,{{start}}:00–{{end}}:00 使用浅色,其余时段深色;顶栏太阳/月亮可随时手动覆盖。',
|
||||
autoHint: '按所选时区 {{start}}:00–{{end}}:00 浅色,其余深色;顶栏可随时手动切换。',
|
||||
},
|
||||
prefsCard: {
|
||||
title: '外观与语言',
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
emptyDescription: '添加第一个 Loki 存储后端',
|
||||
// 删除确认文案(有部署引用时的警告;count 走中文无复数)
|
||||
deleteWarning:
|
||||
'确定要删除「{{name}}」吗?警告:仍有 {{count}} 个部署使用该存储,删除后这些部署的对象存储配置将失效。',
|
||||
'确定删除「{{name}}」吗?仍有 {{count}} 个部署在使用,删除后其对象存储将失效。',
|
||||
errDelete: '删除存储配置失败',
|
||||
errSetDefault: '设置默认存储失败',
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
noEmail: '无邮箱',
|
||||
empty: '目录下没有用户',
|
||||
importing: '导入中...',
|
||||
importCount_other: '导入 {{count}} 个用户',
|
||||
importCount: '导入 {{count}} 个用户',
|
||||
importFailed: '导入失败',
|
||||
loadFailed: '加载 LDAP 用户失败',
|
||||
},
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
clear: '清除选择',
|
||||
selectAll: '全选',
|
||||
confirmTitle: '确认批量删除',
|
||||
confirmDesc: '确定要删除选中的 {{count}} 个用户吗?受保护的账号(自己/最后一个管理员)将被跳过,此操作不可恢复。',
|
||||
confirmDesc: '删除选中的 {{count}} 个用户?受保护账号会跳过,操作不可恢复。',
|
||||
skippedWarn: '{{count}} 项被跳过:{{reasons}}',
|
||||
failed: '批量删除失败',
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ const wizard = {
|
||||
ready: '已就绪',
|
||||
connected: '已接入:{{names}}',
|
||||
nameSep: '、',
|
||||
k8sHint: '通过 kubeconfig 或 ServiceAccount Token 接入集群,创建后自动验证连通性',
|
||||
k8sHint: 'kubeconfig 或 Token 接入,创建后自动验证连通性。',
|
||||
addCluster: '添加集群',
|
||||
hostHint: 'SSH 密钥或密码接入主机(用于部署与后续采集器安装)',
|
||||
addHost: '添加主机',
|
||||
@@ -45,7 +45,7 @@ const wizard = {
|
||||
manage: '管理存储',
|
||||
add: '添加存储',
|
||||
configure: '配置存储',
|
||||
hint: '推荐对象存储(MinIO / RustFS / S3 / OSS):多副本与数据保留更可靠。添加时可用「测试连接」验证凭证,并自动创建不存在的桶;跳过则使用本地磁盘。',
|
||||
hint: '推荐对象存储(MinIO / RustFS / S3 / OSS),更可靠;跳过则使用本地磁盘。',
|
||||
},
|
||||
deploy: {
|
||||
meta: '{{mode}} · 命名空间 {{namespace}} · {{endpoint}}',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
import { BellRing, Plus, Play, Pencil, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@@ -380,14 +381,12 @@ const AlertsPage = memo(function AlertsPage() {
|
||||
const checked = field.value.includes(String(ch.id))
|
||||
return (
|
||||
<label key={ch.id} className={`flex items-center gap-2 text-sm cursor-pointer rounded-md border px-2.5 py-1.5 ${checked ? 'border-primary bg-primary/5 text-primary' : 'border-border'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) field.onChange([...field.value, String(ch.id)])
|
||||
onCheckedChange={(v) => {
|
||||
if (v) field.onChange([...field.value, String(ch.id)])
|
||||
else field.onChange(field.value.filter((x) => x !== String(ch.id)))
|
||||
}}
|
||||
className="accent-primary"
|
||||
/>
|
||||
{ch.name}
|
||||
{!ch.enabled && <span className="text-xs text-muted-foreground">{t('form.channelDisabled')}</span>}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { Collector } from '@/types'
|
||||
|
||||
// 表单 schema:错误消息经 t 注入(随语言重建,RHF 每次渲染刷新 resolver)
|
||||
@@ -153,7 +154,7 @@ export const CollectorEditDialog = memo(function CollectorEditDialog({ collector
|
||||
{nsError && <p className="text-xs text-destructive">{nsError}</p>}
|
||||
<div className="rounded-md border">
|
||||
<label className="flex items-center gap-2 border-b px-3 py-2 text-sm font-medium cursor-pointer hover:bg-muted/50">
|
||||
<input type="checkbox" checked={allSelected} onChange={setAll} />
|
||||
<Checkbox checked={allSelected} onCheckedChange={() => setAll()} />
|
||||
{t('edit.allCheckbox')}
|
||||
</label>
|
||||
<div className="p-2">
|
||||
|
||||
@@ -60,7 +60,7 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{d.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{d.mode} · {d.namespace || 'loki'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ResourceListCard = memo(function ResourceListCard({
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs flex-shrink-0 ${getStatusColorClass(
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${getStatusColorClass(
|
||||
getItemStatus(item)
|
||||
)}`}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
label={t('stat.totalDeployments')}
|
||||
value={stats.totalDeployments}
|
||||
icon={Rocket}
|
||||
iconColor="text-primary"
|
||||
iconColor="text-success"
|
||||
iconBg="bg-success/10"
|
||||
sub={{
|
||||
icon: TrendingUp,
|
||||
@@ -100,7 +100,7 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
{/* 快速开始引导:尚无任何部署时置顶(状态推导式引导) */}
|
||||
{(data?.stats.totalDeployments ?? 0) === 0 && (
|
||||
<Card className="border-primary/30 bg-primary/5">
|
||||
<CardContent className="p-5 flex items-center justify-between flex-wrap gap-3">
|
||||
<CardContent className="p-6 flex items-center justify-between flex-wrap gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Wand2 className="w-5 h-5 text-primary" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, Tags } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { logsApi } from '@/api/logs'
|
||||
import { applyLabelFilter, extractSelector } from '../query-utils'
|
||||
|
||||
@@ -76,7 +77,7 @@ export function LokiLabelBrowser({ sourceId, query, onQueryChange, start, end }:
|
||||
}
|
||||
}
|
||||
|
||||
return <section className="overflow-hidden rounded-lg border bg-card" aria-label={t('labels.browser')}>
|
||||
return <section className="overflow-hidden rounded-xl border bg-card" aria-label={t('labels.browser')}>
|
||||
<button type="button" className="flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium hover:bg-muted/50" aria-expanded={open} onClick={() => { setOpen(!open); if (!open) setLoadingLabels(true) }}>
|
||||
{open ? <ChevronDown className="size-4" /> : <ChevronRight className="size-4" />}
|
||||
<Tags className="size-4 text-info" />{t('labels.browser')}
|
||||
@@ -88,7 +89,7 @@ export function LokiLabelBrowser({ sourceId, query, onQueryChange, start, end }:
|
||||
</div>
|
||||
<div className="min-w-0 p-3">
|
||||
{!activeLabel ? <p className="py-12 text-center text-sm text-muted-foreground">{t('labels.pickHint')}</p> : <><div className="mb-3 flex flex-wrap items-center gap-2"><strong className="text-sm">{t('labels.valuesOf', { label: activeLabel })}</strong><Input aria-label={t('labels.searchAria', { label: activeLabel })} className="h-8 max-w-sm" placeholder={t('labels.searchPlaceholder')} value={filter} onChange={event => setFilter(event.target.value)} /></div>
|
||||
<label className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground"><input type="checkbox" checked={scoped} onChange={event => setScoped(event.target.checked)} />{t('labels.scoped')}</label>
|
||||
<label className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground"><Checkbox checked={scoped} onCheckedChange={v => setScoped(Boolean(v))} />{t('labels.scoped')}</label>
|
||||
<div className="grid max-h-48 gap-1 overflow-auto sm:grid-cols-2 xl:grid-cols-3">{loadingValues ? <p className="p-2 text-sm text-muted-foreground">{t('labels.loading')}</p> : shown.length === 0 ? <p className="p-2 text-sm text-muted-foreground">{t('labels.noValues')}</p> : shown.map(value => <button key={value} type="button" title={value} onClick={() => choose(activeLabel, value)} className={`truncate rounded px-2 py-1.5 text-left font-mono text-sm ${selected[activeLabel] === value ? 'bg-success/10 text-success' : 'hover:bg-muted'}`}>{value}</button>)}</div>
|
||||
{filter.trim() && !values.includes(filter.trim()) && <button type="button" className="mt-2 rounded border px-2 py-1 text-xs text-primary hover:bg-muted" onClick={() => choose(activeLabel, filter.trim())}>{t('labels.useAsValue', { value: filter.trim() })}</button>}
|
||||
<p className="mt-2 text-xs text-muted-foreground">{t('labels.hint')}</p>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
|
||||
const sourceName = (id: number) => sources.find(source => source.id === id)?.name ?? t('access.sourceRef', { id })
|
||||
|
||||
return <div className="grid gap-6 xl:grid-cols-[minmax(240px,0.85fr)_minmax(0,2fr)]">
|
||||
<aside className="rounded-xl border bg-card p-5 space-y-4">
|
||||
<aside className="rounded-xl border bg-card p-4 space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="font-semibold">{t('access.listTitle')} <span className="text-sm font-normal text-muted-foreground">({groups.length})</span></h2>
|
||||
<Button size="sm" onClick={() => setForm(emptyForm(sources[0]?.id ?? 0))}><Plus className="mr-1 size-4" />{t('access.create')}</Button>
|
||||
@@ -96,7 +96,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
|
||||
{error && <ErrorState message={error} onRetry={() => void reload()} />}
|
||||
{!loading && !error && groups.length === 0 && <p className="text-sm text-muted-foreground">{t('access.empty')}</p>}
|
||||
<div className="space-y-2">{groups.map(group => <button key={group.id} type="button" onClick={() => setForm({ ...group, labels: { ...group.labels }, userIds: [...group.userIds] })} className={`w-full rounded-lg border p-3 text-left transition-colors ${form.id === group.id ? 'border-primary bg-primary/5' : 'hover:bg-muted'}`}>
|
||||
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span>{Object.keys(group.labels).length > 0 ? (
|
||||
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 truncate text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span>{Object.keys(group.labels).length > 0 ? (
|
||||
<span className="mt-1 flex flex-wrap gap-1">
|
||||
{Object.entries(group.labels).map(([key, value]) => <span key={key} className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">{key}={value}</span>)}
|
||||
</span>
|
||||
@@ -105,7 +105,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
|
||||
)}
|
||||
</button>)}</div>
|
||||
</aside>
|
||||
<section className="rounded-xl border bg-card p-5 space-y-6">
|
||||
<section className="rounded-xl border bg-card p-4 space-y-6">
|
||||
<div className="flex items-start justify-between gap-3"><div><h2 className="font-semibold">{form.id ? t('access.editTitle') : t('access.createTitle')}</h2><p className="mt-1 text-sm text-muted-foreground">{t('access.formHint')}</p></div>{form.id && <Button variant="outline" size="sm" className="text-destructive" onClick={() => setDeleteTarget(groups.find(group => group.id === form.id) ?? null)}>{t('access.remove')}</Button>}</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2"><label className="text-sm font-medium">{t('access.nameLabel')}<Input className="mt-1" value={form.name} onChange={event => setForm(current => ({ ...current, name: event.target.value }))} placeholder={t('access.namePlaceholder')} /></label><label className="text-sm font-medium">{t('access.sourceLabel')}
|
||||
<Select
|
||||
|
||||
Reference in new issue
Block a user