From 32beec2229e34b8c57cec9c3bc94a17fc2dc58ef Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Thu, 9 Apr 2026 10:25:15 +0300 Subject: [PATCH] feat(client): add Timestamp component, ESLint 10 + Prettier, and code quality fixes - add Timestamp component with moment relative time and ISO tooltip - add Timestamp stories (JustNow, MinutesAgo, HoursAgo, DaysAgo, MonthsAgo) - add ESLint 10 with typescript-eslint, react-hooks, react-refresh, prettier - add Prettier config with singleQuote, semi, trailingComma all, printWidth 100 - add lint, lint:fix, format, test:storybook scripts to package.json - fix react-hooks/set-state-in-effect in all page components - auto-fix 113 Prettier formatting issues across src and .storybook --- client/.prettierignore | 5 + client/.prettierrc | 8 + client/.storybook/main.ts | 25 +- client/.storybook/preview.tsx | 18 +- .../stories/Breadcrumbs.stories.tsx | 5 +- client/.storybook/stories/Card.stories.tsx | 21 +- client/.storybook/stories/Colors.stories.tsx | 97 +- .../.storybook/stories/SidePanel.stories.tsx | 20 +- client/.storybook/stories/Table.stories.tsx | 16 +- .../.storybook/stories/Timestamp.stories.tsx | 21 + client/eslint.config.mjs | 34 + client/package.json | 15 +- client/src/App.tsx | 14 +- client/src/hooks/useTheme.ts | 2 +- client/src/i18n/index.ts | 14 +- client/src/main.tsx | 14 +- client/src/pages/EnvironmentsPage.tsx | 14 +- client/src/pages/KeysPage.tsx | 8 +- client/src/pages/ScenariosPage.tsx | 27 +- client/src/pages/SessionsPage.tsx | 21 +- client/src/ui/Breadcrumbs/Breadcrumbs.tsx | 20 +- client/src/ui/Button/Button.tsx | 4 +- client/src/ui/Input/Input.tsx | 4 +- client/src/ui/Select/Select.tsx | 22 +- client/src/ui/SidePanel/SidePanel.tsx | 4 +- client/src/ui/Timestamp/Timestamp.module.css | 18 + client/src/ui/Timestamp/Timestamp.tsx | 18 + client/src/ui/index.ts | 3 + package-lock.json | 963 +++++++++++++++--- 29 files changed, 1175 insertions(+), 280 deletions(-) create mode 100644 client/.prettierignore create mode 100644 client/.prettierrc create mode 100644 client/.storybook/stories/Timestamp.stories.tsx create mode 100644 client/eslint.config.mjs create mode 100644 client/src/ui/Timestamp/Timestamp.module.css create mode 100644 client/src/ui/Timestamp/Timestamp.tsx diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 0000000..308b3de --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,5 @@ +dist/ +storybook-static/ +node_modules/ +*.css +*.json diff --git a/client/.prettierrc b/client/.prettierrc new file mode 100644 index 0000000..1d9d0f6 --- /dev/null +++ b/client/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "arrowParens": "always" +} diff --git a/client/.storybook/main.ts b/client/.storybook/main.ts index 03f32f9..931b608 100644 --- a/client/.storybook/main.ts +++ b/client/.storybook/main.ts @@ -1,26 +1,23 @@ import type { StorybookConfig } from '@storybook/react-vite'; -import { dirname } from "path" +import { dirname } from 'path'; -import { fileURLToPath } from "url" +import { fileURLToPath } from 'url'; /** -* This function is used to resolve the absolute path of a package. -* It is needed in projects that use Yarn PnP or are set up within a monorepo. -*/ + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ function getAbsolutePath(value: string) { - return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))) + return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); } const config: StorybookConfig = { - "stories": [ - "./stories/**/*.mdx", - "./stories/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - "addons": [ + stories: ['./stories/**/*.mdx', './stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ getAbsolutePath('@storybook/addon-vitest'), getAbsolutePath('@storybook/addon-a11y'), - getAbsolutePath('@storybook/addon-docs') + getAbsolutePath('@storybook/addon-docs'), ], - "framework": getAbsolutePath('@storybook/react-vite') + framework: getAbsolutePath('@storybook/react-vite'), }; -export default config; \ No newline at end of file +export default config; diff --git a/client/.storybook/preview.tsx b/client/.storybook/preview.tsx index 57d66d6..a1cc1ce 100644 --- a/client/.storybook/preview.tsx +++ b/client/.storybook/preview.tsx @@ -1,6 +1,6 @@ -import React from 'react' -import type { Preview } from '@storybook/react-vite' -import '../src/ui/tokens.css' +import React from 'react'; +import type { Preview } from '@storybook/react-vite'; +import '../src/ui/tokens.css'; const preview: Preview = { globalTypes: { @@ -12,7 +12,7 @@ const preview: Preview = { icon: 'circlehollow', items: [ { value: 'light', icon: 'sun', title: 'Light' }, - { value: 'dark', icon: 'moon', title: 'Dark' }, + { value: 'dark', icon: 'moon', title: 'Dark' }, ], dynamicTitle: true, }, @@ -20,13 +20,13 @@ const preview: Preview = { }, decorators: [ (Story, context) => { - const theme = (context.globals['theme'] as string) ?? 'light' - document.documentElement.setAttribute('data-theme', theme) + const theme = (context.globals['theme'] as string) ?? 'light'; + document.documentElement.setAttribute('data-theme', theme); return (
- ) + ); }, ], parameters: { @@ -40,6 +40,6 @@ const preview: Preview = { test: 'todo', }, }, -} +}; -export default preview \ No newline at end of file +export default preview; diff --git a/client/.storybook/stories/Breadcrumbs.stories.tsx b/client/.storybook/stories/Breadcrumbs.stories.tsx index cce2a08..a18a268 100644 --- a/client/.storybook/stories/Breadcrumbs.stories.tsx +++ b/client/.storybook/stories/Breadcrumbs.stories.tsx @@ -12,10 +12,7 @@ type Story = StoryObj; export const Default: Story = { args: { - items: [ - { label: 'Scenarios', href: '#' }, - { label: 'Login flow' }, - ], + items: [{ label: 'Scenarios', href: '#' }, { label: 'Login flow' }], }, }; diff --git a/client/.storybook/stories/Card.stories.tsx b/client/.storybook/stories/Card.stories.tsx index 350f386..927d65c 100644 --- a/client/.storybook/stories/Card.stories.tsx +++ b/client/.storybook/stories/Card.stories.tsx @@ -30,16 +30,29 @@ export const Default: Story = { export const ScenarioCard: Story = { render: () => ( -
+
Login flow - Passed + + Passed +

Last run 2 minutes ago · 4 steps

- - + +
), diff --git a/client/.storybook/stories/Colors.stories.tsx b/client/.storybook/stories/Colors.stories.tsx index 7e36793..1b428c4 100644 --- a/client/.storybook/stories/Colors.stories.tsx +++ b/client/.storybook/stories/Colors.stories.tsx @@ -32,27 +32,51 @@ const surfaceColors = [ { name: 'Primary fg', token: '--color-primary-fg', hex: '#2D3339' }, ]; -function Swatch({ name, token, hex, role }: { name: string; token: string; hex: string; role?: string }) { +function Swatch({ + name, + token, + hex, + role, +}: { + name: string; + token: string; + hex: string; + role?: string; +}) { return ( -
-
+
+
-
{name}
- {role &&
{role}
} -
{hex}
-
{token}
+
+ {name} +
+ {role && ( +
+ {role} +
+ )} +
+ {hex} +
+
+ {token} +
); @@ -61,12 +85,20 @@ function Swatch({ name, token, hex, role }: { name: string; token: string; hex: function Section({ title, children }: { title: string; children: React.ReactNode }) { return (
-

+

{title}

-
- {children} -
+
{children}
); } @@ -74,18 +106,27 @@ function Section({ title, children }: { title: string; children: React.ReactNode function ColorPalette() { return (
-

Color Palette

+

+ Color Palette +

- All colors are exposed as CSS custom properties on :root via tokens.css. + All colors are exposed as CSS custom properties on :root via{' '} + tokens.css.

- {palette.map(c => )} + {palette.map((c) => ( + + ))}
- {semanticColors.map(c => )} + {semanticColors.map((c) => ( + + ))}
- {surfaceColors.map(c => )} + {surfaceColors.map((c) => ( + + ))}
); diff --git a/client/.storybook/stories/SidePanel.stories.tsx b/client/.storybook/stories/SidePanel.stories.tsx index 0a03f52..ca62dba 100644 --- a/client/.storybook/stories/SidePanel.stories.tsx +++ b/client/.storybook/stories/SidePanel.stories.tsx @@ -5,7 +5,16 @@ const NAV_ITEMS = ['Dashboard', 'Scenarios', 'Environments', 'Reports', 'Setting function NavList() { return ( -
    +
      {NAV_ITEMS.map((item) => (
    • - + + ), }, diff --git a/client/src/pages/SessionsPage.tsx b/client/src/pages/SessionsPage.tsx index 11ac67a..d25a05b 100644 --- a/client/src/pages/SessionsPage.tsx +++ b/client/src/pages/SessionsPage.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { sessions } from '../api'; import type { Session } from '../api'; -import { Badge, Button, Table, type TableColumn } from '../ui'; +import { Badge, Button, Table, type TableColumn, Timestamp } from '../ui'; import styles from './Page.module.css'; export function SessionsPage() { @@ -12,38 +12,39 @@ export function SessionsPage() { const [error, setError] = useState(null); const load = useCallback(() => { - setLoading(true); - sessions.list() + sessions + .list() .then((res) => setItems(res.data)) .catch((err: Error) => setError(err.message)) .finally(() => setLoading(false)); }, []); - useEffect(load, [load]); + useEffect(() => { + load(); + }, [load]); const handleDelete = async (id: number) => { await sessions.remove(id); + setLoading(true); load(); }; const columns: TableColumn[] = [ - { key: 'id', header: t('sessions.col_id'), render: (s) => s.id, width: 60 }, - { key: 'name', header: t('sessions.col_name'), render: (s) => s.sessionName }, + { key: 'id', header: t('sessions.col_id'), render: (s) => s.id, width: 60 }, + { key: 'name', header: t('sessions.col_name'), render: (s) => s.sessionName }, { key: 'status', header: t('sessions.col_status'), width: 100, render: (s) => ( - - {s.status} - + {s.status} ), }, { key: 'lastUsed', header: t('sessions.col_lastUsed'), width: 160, - render: (s) => s.lastUsedAt ? new Date(s.lastUsedAt).toLocaleString() : '—', + render: (s) => (s.lastUsedAt ? : '—'), }, { key: 'actions', diff --git a/client/src/ui/Breadcrumbs/Breadcrumbs.tsx b/client/src/ui/Breadcrumbs/Breadcrumbs.tsx index 83a77e9..e2d38d4 100644 --- a/client/src/ui/Breadcrumbs/Breadcrumbs.tsx +++ b/client/src/ui/Breadcrumbs/Breadcrumbs.tsx @@ -16,7 +16,13 @@ export interface BreadcrumbsProps { export function Breadcrumbs({ items, separator, className }: BreadcrumbsProps) { const sep = separator ?? ( ); @@ -28,11 +34,17 @@ export function Breadcrumbs({ items, separator, className }: BreadcrumbsProps) { return (
    • {isLast ? ( - {item.label} + + {item.label} + ) : item.href ? ( - {item.label} + + {item.label} + ) : ( - + )} {!isLast && {sep}}
    • diff --git a/client/src/ui/Button/Button.tsx b/client/src/ui/Button/Button.tsx index 62136ab..643388f 100644 --- a/client/src/ui/Button/Button.tsx +++ b/client/src/ui/Button/Button.tsx @@ -18,7 +18,9 @@ export function Button({ }: ButtonProps) { return (