Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dialog } from '@clickhouse/click-ui';
import { Dialog, Select } from '@clickhouse/click-ui';
import { useTranslation } from 'react-i18next';
import type * as t from '@/types';
import { useTheme } from '@/contexts/ThemeContext';
import { useLocalize } from '@/hooks';
Expand All @@ -11,9 +12,26 @@ const THEME_LABEL_KEYS: Record<t.ThemeOption, string> = {
dark: 'com_nav_theme_dark',
};

const LANGUAGE_OPTIONS = ['en', 'zh-Hans'] as const;
type LanguageOption = (typeof LANGUAGE_OPTIONS)[number];
const LANGUAGE_LABEL_KEYS: Record<LanguageOption, string> = {
en: 'com_ui_language_english',
'zh-Hans': 'com_ui_language_zh_hans',
};

export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) {
const localize = useLocalize();
const { theme, setTheme } = useTheme();
const { i18n } = useTranslation();

const resolvedLanguage = i18n.resolvedLanguage ?? i18n.language ?? 'en';
const currentLanguage: LanguageOption = resolvedLanguage.startsWith('zh') ? 'zh-Hans' : 'en';

const setLanguage = (language: LanguageOption) => {
i18n.changeLanguage(language);
localStorage.setItem('i18nextLng', language);
document.documentElement.lang = language;
};

return (
<Dialog
Expand All @@ -23,6 +41,7 @@ export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) {
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_ui_settings')}
showClose
onClose={onClose}
Expand Down Expand Up @@ -57,6 +76,26 @@ export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) {
))}
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium text-(--cui-color-text-default)">
{localize('com_ui_language')}
</span>
</div>
<div className="w-40 shrink-0">
<Select
value={currentLanguage}
onSelect={(v) => setLanguage(v as LanguageOption)}
aria-label={localize('com_ui_language')}
>
{LANGUAGE_OPTIONS.map((opt) => (
<Select.Item key={opt} value={opt}>
{localize(LANGUAGE_LABEL_KEYS[opt])}
</Select.Item>
))}
</Select>
</div>
</div>
</div>
</Dialog.Content>
</Dialog>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const navItems: t.NavItem[] = [
capability: [SystemCapabilities.READ_ROLES, SystemCapabilities.READ_GROUPS],
},
{ labelKey: 'com_nav_grants', path: '/grants', icon: 'lock' },
{ labelKey: 'com_nav_tools', path: '/tools', icon: 'plug' },
{ labelKey: 'com_nav_usage', path: '/usage', icon: 'bar-chart' },
{ labelKey: 'com_nav_services', path: '/services', icon: 'server' },
{ labelKey: 'com_nav_help', path: '/help', icon: 'question' },
];

Expand Down
1 change: 1 addition & 0 deletions src/components/access/CreateGroupDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function CreateGroupDialog({ open, onClose }: t.CreateGroupDialogProps) {
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_access_create_group')}
showClose
onClose={resetAndClose}
Expand Down
1 change: 1 addition & 0 deletions src/components/access/CreateRoleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function CreateRoleDialog({ open, onClose }: t.CreateRoleDialogProps) {
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_access_create_role')}
showClose
onClose={resetAndClose}
Expand Down
1 change: 1 addition & 0 deletions src/components/access/EditGroupDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function EditGroupDialog({ group, canManage, onClose }: t.EditGroupDialog
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_access_edit_group')}
showClose
onClose={onClose}
Expand Down
1 change: 1 addition & 0 deletions src/components/access/EditRoleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_access_edit_role')}
showClose
onClose={onClose}
Expand Down
1 change: 1 addition & 0 deletions src/components/configuration/ConfirmSaveDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function ConfirmSaveDialog({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_config_confirm_save_title')}
showClose
onClose={onCancel}
Expand Down
1 change: 1 addition & 0 deletions src/components/configuration/DeleteProfileValueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function DeleteProfileValueModal({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_scope_confirm_remove')}
showClose
onClose={onCancel}
Expand Down
1 change: 1 addition & 0 deletions src/components/configuration/ImportYamlDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export function ImportYamlDialog({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_config_import_yaml_title')}
showClose
onClose={handleClose}
Expand Down
1 change: 1 addition & 0 deletions src/components/configuration/ProfileIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function ProfileIndicator({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_scope_cascade_title')}
showClose
onClose={handleClose}
Expand Down
3 changes: 2 additions & 1 deletion src/components/configuration/ProfileValueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export function ProfileValueModal({
if (!isOpen) onCancel();
}}
>
<Dialog.Content title={title} showClose onClose={onCancel} className="modal-frost">
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()} title={title} showClose onClose={onCancel} className="modal-frost">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
{scopeConfig && (
Expand Down
1 change: 1 addition & 0 deletions src/components/configuration/ResetBaseConfigDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function ResetBaseConfigDialog({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={localize('com_config_reset_base_title')}
showClose
onClose={onCancel}
Expand Down
18 changes: 18 additions & 0 deletions src/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ const QUICK_LINKS: (t.NavItem & { descKey: string })[] = [
icon: 'question',
descKey: 'com_dash_help_desc',
},
{
labelKey: 'com_nav_tools',
path: '/tools',
icon: 'plug',
descKey: 'com_dash_tools_desc',
},
{
labelKey: 'com_nav_usage',
path: '/usage',
icon: 'bar-chart',
descKey: 'com_dash_usage_desc',
},
{
labelKey: 'com_nav_services',
path: '/services',
icon: 'server',
descKey: 'com_dash_services_desc',
},
];

export function DashboardPage() {
Expand Down
4 changes: 4 additions & 0 deletions src/components/grants/AuditLogDetailDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export function AuditLogDetailDrawer({
)}
/>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
aria-label={localize('com_audit_detail_title')}
onEscapeKeyDown={() => onClose()}
className={cn(
Expand Down Expand Up @@ -270,6 +271,7 @@ export function AuditLogDetailDrawer({
)}
/>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
aria-label={localize('com_audit_detail_title')}
onEscapeKeyDown={() => onClose()}
className={cn(
Expand Down Expand Up @@ -322,6 +324,7 @@ export function AuditLogDetailDrawer({
)}
/>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
aria-label={localize('com_audit_detail_title')}
onEscapeKeyDown={() => onClose()}
className={cn(
Expand Down Expand Up @@ -387,6 +390,7 @@ export function AuditLogDetailDrawer({
)}
/>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
aria-label={localize('com_audit_detail_title')}
onEscapeKeyDown={() => onClose()}
className={cn(
Expand Down
51 changes: 7 additions & 44 deletions src/components/grants/AuditLogTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
exportAuditLogServerFn,
} from '@/server';
import {
DatePickerCell,
EmptyState,
LoadingState,
Pagination,
Expand All @@ -41,53 +42,15 @@ const TARGET_TYPE_OPTIONS: readonly PrincipalType[] = [
PrincipalType.GROUP,
PrincipalType.ROLE,
] as const;
const TARGET_TYPE_LABEL_KEYS: Record<(typeof TARGET_TYPE_OPTIONS)[number], string> = {
[PrincipalType.USER]: 'com_audit_target_user',
[PrincipalType.GROUP]: 'com_audit_target_group',
[PrincipalType.ROLE]: 'com_audit_target_role',
};
/** Radix `Select.Item` cannot use `value=""` (Radix reserves empty string for
* "no selection"). Use a non-empty sentinel and translate to `''` in state. */
const TARGET_TYPE_ALL = '__all__';

/**
* Wraps a click-ui DatePicker so only the trigger button is tab-focusable.
* click-ui renders both a PopoverTrigger button AND an inner readonly input,
* which produces two stops in the tab order. The input has no exposed `tabIndex`
* prop, so we reach for the DOM node and set it to -1. The class hooks the
* CSS rule that rounds the trigger's focus outline to match the wrapper border.
*
* `resetKey` is the caller's signal that the inner DatePicker has been keyed
* to remount (e.g. the Clear button bumping a nonce): the inner `<input>` is
* replaced and the previous patch is lost, so the effect must re-run and
* re-apply `tabIndex = -1` against the fresh DOM node.
*/
function DatePickerCell({
children,
resetKey,
inputId,
}: {
children: React.ReactNode;
resetKey?: unknown;
/**
* Stamps the click-ui-rendered `<input>` with an `id` so an external
* `<label htmlFor={...}>` (and the e2e WCAG check) can target it. click-ui
* has no `id` prop, so we apply it via DOM ref in the same effect that
* removes the input from the tab order.
*/
inputId?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const node = ref.current;
if (!node) return;
const input = node.querySelector('input');
if (!input) return;
input.tabIndex = -1;
if (inputId) input.id = inputId;
}, [resetKey, inputId]);
return (
<div ref={ref} className="audit-date-cell contents">
{children}
</div>
);
}

function downloadBlob(blob: Blob): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
Expand Down Expand Up @@ -510,7 +473,7 @@ export function AuditLogTab() {
<Select.Item value={TARGET_TYPE_ALL}>{localize('com_ui_all')}</Select.Item>
{TARGET_TYPE_OPTIONS.map((pt) => (
<Select.Item key={pt} value={pt}>
{pt}
{localize(TARGET_TYPE_LABEL_KEYS[pt])}
</Select.Item>
))}
</Select>
Expand Down
1 change: 1 addition & 0 deletions src/components/grants/EditCapabilitiesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function EditCapabilitiesDialog({
}}
>
<Dialog.Content
onInteractOutside={(event) => event.preventDefault()}
title={dialogTitle}
showClose
onClose={onClose}
Expand Down
Loading