Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a08069c
fix(table): set table-body-sort-bg per design spec
Fiona2016 May 12, 2026
5ff8a6d
fix(table): force header nowrap per design rule §5
Fiona2016 May 12, 2026
ee0c0cc
fix(table): default pageSize 15 on alert-mutes / alert-subscribes / j…
Fiona2016 May 12, 2026
1d3dedc
fix(alert-subscribes): combine name + business group into Column1
Fiona2016 May 12, 2026
f06da5e
fix(alert-mutes): collapse name + datasource type + business group in…
Fiona2016 May 12, 2026
53afed2
fix(job-tpls): collapse title + ID + business group into Column1
Fiona2016 May 12, 2026
efa0f94
fix(job-tasks): collapse title + ID + business group into Column1
Fiona2016 May 12, 2026
3d661d8
fix(users): collapse identity columns into Column1
Fiona2016 May 12, 2026
f5b951b
fix(alert-rules): collapse name + cate + business group + severity in…
Fiona2016 May 12, 2026
69be9a3
fix(table): consolidate row actions into overflow menus
Fiona2016 May 13, 2026
44bce41
fix(table): use overflow menus on route list pages
Fiona2016 May 13, 2026
412fdac
fix(table): collapse remaining menu route actions
Fiona2016 May 13, 2026
ad19f99
feat(table): add shared action dropdown
Fiona2016 May 13, 2026
194da29
fix(table): align ai config actions
Fiona2016 May 13, 2026
0555ce2
fix(table): align notification actions
Fiona2016 May 13, 2026
e44a95f
fix(table): align alert event actions
Fiona2016 May 13, 2026
2fc3e1f
fix(table): align builtin component actions
Fiona2016 May 13, 2026
77a7986
fix(table): align data route actions
Fiona2016 May 13, 2026
d39465f
fix(table): align task and user actions
Fiona2016 May 13, 2026
e5b4a5b
fix(table): forward action dropdown trigger events
Fiona2016 May 13, 2026
573387d
Merge remote-tracking branch 'origin/main' into feat-table-design-srm…
Fiona2016 May 14, 2026
84b5a39
Merge remote-tracking branch 'origin/main' into feat-table-design-srm…
Fiona2016 May 15, 2026
439d4f1
refactor(table): compact primary list metadata columns
Fiona2016 May 15, 2026
d29310e
feat(table): refine users list primary column
Fiona2016 May 17, 2026
920cd99
feat(table): refine sorter interaction
Fiona2016 May 18, 2026
1934313
feat(table): unify route table tags
Fiona2016 May 18, 2026
0871dc4
style(table): align sorted column body bg with header
Fiona2016 May 18, 2026
1d4e465
fix(table): keep fixed cells opaque
Fiona2016 May 18, 2026
fab7e0a
feat(table): fix route table action columns
Fiona2016 May 18, 2026
617fcfe
fix(table): remove measure-row collapse and tune fixed-column edge
Fiona2016 May 18, 2026
73b5a4c
fix(table): stabilize task table layouts
Fiona2016 May 18, 2026
552d2b4
fix(table): refine dropdown interactions and sorter style
Fiona2016 May 18, 2026
46c0542
fix(table): align sorter icon with title
Fiona2016 May 18, 2026
8142e2e
fix(table): use fill-2-5 for sorted column bg
Fiona2016 May 18, 2026
dad392d
fix(table): remove sorted column color override
Fiona2016 May 18, 2026
f74a5cc
feat(alerts): add event tag collapse toggle
Fiona2016 May 18, 2026
5de23be
fix(table): apply acceptance feedback on 0517
Fiona2016 May 20, 2026
0691219
feat(table): normalize table action dropdown alignment and trigger
jsers May 20, 2026
60ac063
fix(table): comment out selected row styles to prevent fixed column b…
jsers May 20, 2026
1a8f2d9
feat(alertRules): replace event count column icons with Tags component
jsers May 20, 2026
79c38e1
refactor(tags): relocate Tags component to shared directory and adopt…
jsers May 20, 2026
3bf95de
feat(alertRules): replace TableTags with generic Tags and add severit…
jsers May 20, 2026
a53ed5c
refactor(alertRules): render null instead of '-' for empty datasource…
jsers May 20, 2026
d27dde7
refactor(table): unify table styling and Tags component usage
jsers May 20, 2026
5338bb0
refactor(table): unify tag rendering and replace TableTags with Tags …
jsers May 21, 2026
0d17533
refactor: add explicit type annotations to callback parameters
jsers May 21, 2026
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
8 changes: 4 additions & 4 deletions scripts/generate_antd_dark_less.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ function saveLess(filePath, filename, callback) {
'table-header-bg': 'var(--fc-fill-2-5)',
'table-header-color': 'var(--fc-text-3)',
'table-header-sort-bg': 'var(--fc-fill-2-5)',
'table-body-sort-bg': 'var(--fc-fill-2-5)',
'table-body-sort-bg': 'var(--fc-fill-2)',
'table-row-hover-bg': 'rgb(var(--fc-fill-5-rgb) / 0.2)',
'table-selected-row-color': 'inherit',
// Keep Less color functions compile-safe; runtime CSS vars are patched in theme/default.less.
// AntD calls color functions on selected/border tokens; patch runtime CSS vars in theme/default.less.
'table-selected-row-bg': 'rgba(228, 228, 231, 0.15)',
'table-body-selected-sort-bg': '@table-selected-row-bg',
'table-selected-row-hover-bg': 'rgba(228, 228, 231, 0.25)',
Expand All @@ -73,8 +73,8 @@ function saveLess(filePath, filename, callback) {
'table-font-size-md': '14px',
'table-font-size-sm': '@table-font-size',
'table-header-cell-split-color': 'var(--fc-border-color)',
'table-header-sort-active-bg': 'rgb(var(--fc-fill-5-rgb) / 0.4)',
'table-fixed-header-sort-active-bg': 'var(--fc-fill-3)',
'table-header-sort-active-bg': 'var(--fc-fill-2-5)',
'table-fixed-header-sort-active-bg': 'var(--fc-fill-2-5)',
'border-radius-base': '8px',
'border-radius-sm': '4px',
'checkbox-border-radius': '2px',
Expand Down
8 changes: 4 additions & 4 deletions scripts/generate_antd_gold_less.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ function saveLess(filePath, filename, callback) {
'table-header-bg': 'var(--fc-fill-2-5)',
'table-header-color': 'var(--fc-text-3)',
'table-header-sort-bg': 'var(--fc-fill-2-5)',
'table-body-sort-bg': 'var(--fc-fill-2-5)',
'table-body-sort-bg': 'var(--fc-fill-2)',
'table-row-hover-bg': 'rgb(var(--fc-fill-5-rgb) / 0.2)',
'table-selected-row-color': 'inherit',
// Keep Less color functions compile-safe; runtime CSS vars are patched in theme/default.less.
// AntD calls color functions on selected/border tokens; patch runtime CSS vars in theme/default.less.
'table-selected-row-bg': 'rgba(228, 228, 231, 0.15)',
'table-body-selected-sort-bg': '@table-selected-row-bg',
'table-selected-row-hover-bg': 'rgba(228, 228, 231, 0.25)',
Expand All @@ -75,8 +75,8 @@ function saveLess(filePath, filename, callback) {
'table-font-size-md': '14px',
'table-font-size-sm': '@table-font-size',
'table-header-cell-split-color': 'var(--fc-border-color)',
'table-header-sort-active-bg': 'rgb(var(--fc-fill-5-rgb) / 0.4)',
'table-fixed-header-sort-active-bg': 'var(--fc-fill-3)',
'table-header-sort-active-bg': 'var(--fc-fill-2-5)',
'table-fixed-header-sort-active-bg': 'var(--fc-fill-2-5)',
'border-radius-base': '8px',
'border-radius-sm': '4px',
'checkbox-border-radius': '2px',
Expand Down
64 changes: 53 additions & 11 deletions src/App.less
Original file line number Diff line number Diff line change
Expand Up @@ -381,23 +381,65 @@ input::placeholder {
}
}

// antd表格排序icon特殊处理
.ant-table-column-has-sorters .ant-table-column-sorter-inner {
.ant-table-thead > tr > th {
.ant-table-column-sorters {
justify-content: flex-start;
gap: 4px;
min-width: 0;
}

.ant-table-column-title {
flex: 0 1 auto;
min-width: 0;
}

.ant-table-column-sorter {
flex: none;
margin-left: 0;
}
}

// 2026-05 table 设计规范:排序 icon 替换为线性箭头,hover 横向并排 ↑↓,已排序只显示当前方向
.ant-table-column-sorter {
.ant-table-column-sorter-inner {
flex-direction: row;
align-items: center;
gap: 2px;
}

.ant-table-column-sorter-up,
.ant-table-column-sorter-down {
transition: all 0.2s;
width: 12px;
height: 12px;
background-color: currentColor;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;

svg {
display: none;
}
}

&:has(.ant-table-column-sorter-up.active) {
.ant-table-column-sorter-down {
opacity: 0;
}
.ant-table-column-sorter-up {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='64 64 896 896'%3E%3Cpath d='M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='64 64 896 896'%3E%3Cpath d='M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z'/%3E%3C/svg%3E");
}

&:has(.ant-table-column-sorter-down.active) {
.ant-table-column-sorter-up {
opacity: 0;
}
.ant-table-column-sorter-down {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='64 64 896 896'%3E%3Cpath d='M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0048.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='64 64 896 896'%3E%3Cpath d='M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0048.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z'/%3E%3C/svg%3E");
}
}

// 已排序时,非活跃方向的箭头隐藏不占空间
.ant-table-column-has-sorters .ant-table-column-sorter-inner {
&:has(.ant-table-column-sorter-up.active) .ant-table-column-sorter-down,
&:has(.ant-table-column-sorter-down.active) .ant-table-column-sorter-up {
display: none;
}
}

Expand Down
77 changes: 77 additions & 0 deletions src/components/TableActionDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { Button } from 'antd';
import type { ButtonProps } from 'antd/lib/button';
import {
CheckCircle,
Copy,
ExternalLink,
Eye,
Link as LinkIcon,
MoreVertical,
Network,
Pencil,
Play,
Search,
Settings,
ShieldCheck,
Sparkles,
Trash2,
} from 'lucide-react';
import classNames from 'classnames';
import { Link, LinkProps } from 'react-router-dom';

const tableActionIconMap = {
default: CheckCircle,
edit: Pencil,
view: Eye,
settings: Settings,
access: Network,
permission: ShieldCheck,
copy: Copy,
delete: Trash2,
run: Play,
search: Search,
open: ExternalLink,
link: LinkIcon,
ai: Sparkles,
};

export type TableActionIconName = keyof typeof tableActionIconMap;

export function TableActionIcon({ name }: { name: TableActionIconName }) {
const Icon = tableActionIconMap[name];
return <Icon className='fc-table-action-menu-icon' />;
}

interface TableActionButtonProps extends Omit<ButtonProps, 'icon'> {
actionIcon?: TableActionIconName;
icon?: React.ReactNode;
}

export function TableActionButton({ actionIcon, icon, className, type = 'link', ...rest }: TableActionButtonProps) {
return (
<Button
type={type}
className={classNames('fc-table-action-menu-button', className)}
icon={icon || (actionIcon ? <TableActionIcon name={actionIcon} /> : undefined)}
{...rest}
/>
);
}

interface TableActionLinkProps extends LinkProps {
actionIcon?: TableActionIconName;
}

export function TableActionLink({ actionIcon, className, children, ...rest }: TableActionLinkProps) {
return (
<Link className={classNames('fc-table-action-menu-link', className)} {...rest}>
{actionIcon && <TableActionIcon name={actionIcon} />}
<span>{children}</span>
</Link>
);
}

export const TableActionTrigger = React.forwardRef<HTMLElement, ButtonProps>(function TableActionTrigger({ type = 'text', icon, ...rest }, ref) {
return <Button ref={ref as any} type={type} icon={icon || <MoreVertical size={16} />} {...rest} />;
});
147 changes: 147 additions & 0 deletions src/components/TableTags/Tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/// <reference types="jest" />

// mock 掉有副作用或 ESM 专属语法的依赖,只测纯逻辑
jest.mock('@/utils', () => ({ copy2ClipBoard: jest.fn() }));
jest.mock('react', () => ({}));
jest.mock('antd', () => ({}));
jest.mock('@ant-design/icons', () => ({}));
jest.mock('react-i18next', () => ({}));

import { calcLayout } from './Tags';

// GAP = 2(与源码保持一致)

describe('calcLayout', () => {
// ────────────────────── 边界情况 ──────────────────────

it('空数组时返回 visibleCount=0, overflowCount=0', () => {
expect(calcLayout([], 40, 200)).toEqual({ visibleCount: 0, overflowCount: 0 });
});

it('containerWidth <= 0 时直接返回所有 tag 可见', () => {
expect(calcLayout([50, 60, 70], 40, 0)).toEqual({ visibleCount: 3, overflowCount: 0 });
expect(calcLayout([50, 60, 70], 40, -1)).toEqual({ visibleCount: 3, overflowCount: 0 });
});

it('单个 tag 且恰好填满第一行', () => {
expect(calcLayout([100], 30, 100)).toEqual({ visibleCount: 1, overflowCount: 0 });
});

it('单个 tag 且宽度被容器宽度截断后仍可放入第一行', () => {
// 宽 300 > 容器 200,clamp 后为 200,恰好放入
expect(calcLayout([300], 30, 200)).toEqual({ visibleCount: 1, overflowCount: 0 });
});

// ────────────────────── 第一行全部放下 ──────────────────────

it('所有 tag 均可放入第一行(宽松容器)', () => {
// 50 + 2+60 + 2+70 = 184 <= 300
expect(calcLayout([50, 60, 70], 40, 300)).toEqual({ visibleCount: 3, overflowCount: 0 });
});

it('所有 tag 恰好填满第一行(边界等于)', () => {
// 100 + (2+48) = 150
expect(calcLayout([100, 48], 30, 150)).toEqual({ visibleCount: 2, overflowCount: 0 });
});

it('只有一个 tag 放入第一行,其余全部溢出(无法放入第二行)', () => {
// 容器 120;第一行:100 → rem=20
// 第二行 r2start=1:tag2=90 (isFirst,isLast); rem(120)>=90 → r2count=1
// visibleCount=2, overflowCount=0
expect(calcLayout([100, 90], 30, 120)).toEqual({ visibleCount: 2, overflowCount: 0 });
});

// ────────────────────── 两行布局(有溢出指示器)──────────────────────

it('第二行放入部分 tag,剩余作为溢出', () => {
// 容器 120, overflowTag=30
// 第一行:50(rem=70) + 2+50=52(rem=18) → row1End=1;第三个放不下
// 第二行 r2start=2:
// i=2(isFirst, not isLast): needed=50, 需 50+2+30=82 <= 120 ✓ → rem=70, r2count=1
// i=3(not isFirst, not isLast): needed=2+50=52, 需 52+2+30=84 <= 70? ✗ → break
// visibleCount=3, overflowCount=4
expect(calcLayout([50, 50, 50, 50, 50, 50, 50], 30, 120)).toEqual({ visibleCount: 3, overflowCount: 4 });
});

it('第二行最后一个 tag 恰好能放入(isLast 分支)', () => {
// 容器 100, overflowTag=30
// 第一行:50(rem=50);2+60=62 > 50 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, not isLast): needed=60, 需 60+2+30=92 <= 100 ✓ → rem=40, r2count=1
// i=2(not isFirst, isLast): needed=2+30=32 <= 40 ✓ → r2count=2, break
// visibleCount=3, overflowCount=0
expect(calcLayout([50, 60, 30], 30, 100)).toEqual({ visibleCount: 3, overflowCount: 0 });
});

it('第二行最后一个 tag 放不下(isLast 分支,空间不足)', () => {
// 容器 100, overflowTag=30
// 第一行:50(rem=50);2+60=62 > 50 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, not isLast): needed=60, 需 60+2+30=92 <= 100 ✓ → rem=40, r2count=1
// i=2(not isFirst, isLast): needed=2+50=52 > 40 → 不放入, break
// visibleCount=2, overflowCount=1
expect(calcLayout([50, 60, 50], 30, 100)).toEqual({ visibleCount: 2, overflowCount: 1 });
});

it('第二行第一个 tag 就放不下时一个也不放', () => {
// 容器 60, overflowTag=30
// 第一行:50(rem=10);2+50=52 > 10 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, not isLast): needed=50; 需 50+2+30=82 > 60 → break, r2count=0
// visibleCount=1, overflowCount=2
expect(calcLayout([50, 50, 50], 30, 60)).toEqual({ visibleCount: 1, overflowCount: 2 });
});

// ────────────────────── tag 宽度被截断 ──────────────────────

it('tag 宽度超过容器宽度时被截断为容器宽度', () => {
// tagWidths=[200,100], 容器 150 → widths=[150,100]
// 第一行:150(rem=0);2+100=102 > 0 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, isLast): needed=100 <= 150 ✓ → r2count=1, break
// visibleCount=2, overflowCount=0
expect(calcLayout([200, 100], 30, 150)).toEqual({ visibleCount: 2, overflowCount: 0 });
});

it('多个 tag 均超出容器宽度时均被截断', () => {
// 每个 tag 截断为 100,容器 100, overflowTag=30
// 第一行:100(rem=0);2+100=102 > 0 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, not isLast): needed=100; 需 100+2+30=132 > 100 → break
// visibleCount=1, overflowCount=2
expect(calcLayout([200, 300, 400], 30, 100)).toEqual({ visibleCount: 1, overflowCount: 2 });
});

// ────────────────────── 第一行只放得下一个 tag ──────────────────────

it('第一行恰好放一个、第二行剩余全部放下(均为最后)', () => {
// 容器 80, overflowTag=20
// 第一行:60(rem=20);2+60=62 > 20 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, isLast): needed=60 <= 80 ✓ → r2count=1, break
// visibleCount=2, overflowCount=0
expect(calcLayout([60, 60], 20, 80)).toEqual({ visibleCount: 2, overflowCount: 0 });
});

// ────────────────────── overflowTagWidth 影响 ──────────────────────

it('overflowTagWidth 较大时第二行能放入的 tag 更少', () => {
// 容器 200, overflowTag=100
// 第一行:60(rem=140);2+60=62(rem=78);2+60=62 > 78? 78>=62 ✓(rem=16);2+60=62 > 16 → row1End=2
// row1End=2 !== 4 → 进入第二行
// 第二行 r2start=3:
// i=3(isFirst, not isLast): needed=60; 需 60+2+100=162 <= 200 ✓ → rem=140, r2count=1
// i=4(not isFirst, isLast): needed=2+60=62 <= 140 ✓ → r2count=2, break
// visibleCount=5, overflowCount=0
expect(calcLayout([60, 60, 60, 60, 60], 100, 200)).toEqual({ visibleCount: 5, overflowCount: 0 });
});

it('overflowTagWidth 较大导致第二行无法放入任何 tag', () => {
// 容器 80, overflowTag=80
// 第一行:40(rem=40);2+40=42 > 40 → row1End=0
// 第二行 r2start=1:
// i=1(isFirst, not isLast): needed=40; 需 40+2+80=122 > 80 → break
// visibleCount=1, overflowCount=2
expect(calcLayout([40, 40, 40], 80, 80)).toEqual({ visibleCount: 1, overflowCount: 2 });
});
});
Loading