refine Semi UI mobile alert rule editing

This commit is contained in:
lingniu
2026-07-18 16:19:40 +08:00
parent f28dfebaae
commit cedb4f66b4
4 changed files with 302 additions and 66 deletions

View File

@@ -284,6 +284,46 @@ test('uses the shared Semi bottom SideSheet pattern for mobile advanced filters'
expect(document.querySelector('.v2-alert-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' }); expect(document.querySelector('.v2-alert-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' });
}); });
test('keeps mobile rule selection focused and opens editing in a Semi bottom SideSheet', async () => {
layout.mobile = true;
mocks.alertRulesV2.mockResolvedValue([alertRule()]);
mocks.metricCatalog.mockResolvedValue({
metrics: [{ key: 'speed_kmh', label: '速度', unit: 'km/h', category: 'driving', valueType: 'numeric', protocols: ['JT808'], sourceFields: {}, searchable: true, chartable: true, alertable: true }],
asOf: ''
});
mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=rules']}><AlertsPage /></MemoryRouter></QueryClientProvider>);
const ruleItem = await screen.findByRole('button', { name: /测试超速规则/ });
await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true'));
expect(screen.getByRole('region', { name: '告警规则列表' })).toBeInTheDocument();
expect(ruleItem).toHaveAttribute('aria-haspopup', 'dialog');
expect(ruleItem).toHaveAttribute('aria-expanded', 'false');
expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument();
fireEvent.click(ruleItem);
const editor = await screen.findByRole('dialog', { name: '告警规则编辑' });
expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom');
expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' });
expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument();
expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument();
expect(ruleItem).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(within(editor).getByRole('button', { name: '关闭告警规则编辑' }));
await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument());
expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveClass('semi-sidesheet-animation-content_hide_bottom');
expect(ruleItem).toHaveAttribute('aria-expanded', 'false');
const newRule = screen.getByRole('button', { name: /新建规则/ });
expect(newRule).toHaveAttribute('aria-haspopup', 'dialog');
fireEvent.click(newRule);
await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).toBeInTheDocument());
const newEditor = screen.getByRole('dialog', { name: '告警规则编辑' });
expect(within(newEditor).getByText('新建规则')).toBeInTheDocument();
expect(newRule).toHaveAttribute('aria-expanded', 'true');
});
test('creates auditable drafts from simple offline and hydrogen rule templates', async () => { test('creates auditable drafts from simple offline and hydrogen rule templates', async () => {
mocks.alertRulesV2.mockResolvedValue([alertRule()]); mocks.alertRulesV2.mockResolvedValue([alertRule()]);
mocks.metricCatalog.mockResolvedValue({ mocks.metricCatalog.mockResolvedValue({

View File

@@ -309,56 +309,57 @@ function ruleDraft(rule: AlertRule): AlertRuleInput {
function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) { function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mobileLayout = useMobileLayout();
const [selectedID, setSelectedID] = useState(''); const [selectedID, setSelectedID] = useState('');
const [draft, setDraft] = useState<AlertRuleInput>(emptyRule()); const [draft, setDraft] = useState<AlertRuleInput>(emptyRule());
const [editorOpen, setEditorOpen] = useState(false);
useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]); useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]);
const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } }); const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } }); const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
useSideSheetA11y(mobileLayout && editorOpen, '.v2-alert-rule-editor-sidesheet', 'v2-alert-rule-editor', '告警规则编辑', '关闭告警规则编辑');
const mutationError = save.error ?? toggle.error; const mutationError = save.error ?? toggle.error;
const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS; const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS;
const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType); const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType);
const catalogLabels = Object.fromEntries(metrics.map((metric) => [metric.key, metric.label])); const catalogLabels = Object.fromEntries(metrics.map((metric) => [metric.key, metric.label]));
const setList = (key: 'scopeProtocols' | 'scopeVins' | 'scopeOems' | 'scopeModels' | 'scopeCompanies', value: string) => setDraft({ ...draft, [key]: value.split(',').map((item) => item.trim()).filter(Boolean) }); const setList = (key: 'scopeProtocols' | 'scopeVins' | 'scopeOems' | 'scopeModels' | 'scopeCompanies', value: string) => setDraft({ ...draft, [key]: value.split(',').map((item) => item.trim()).filter(Boolean) });
const openNewRule = () => {
setSelectedID('__new__');
setDraft(emptyRule());
if (mobileLayout) setEditorOpen(true);
};
const openRule = (rule: AlertRule) => {
setSelectedID(rule.id);
setDraft(ruleDraft(rule));
if (mobileLayout) setEditorOpen(true);
};
const applyTemplate = (template: RuleTemplate) => { const applyTemplate = (template: RuleTemplate) => {
setSelectedID('__new__'); setSelectedID('__new__');
setDraft({ ...template.draft, scopeProtocols: [...template.draft.scopeProtocols], notificationChannels: [...template.draft.notificationChannels] }); setDraft({ ...template.draft, scopeProtocols: [...template.draft.scopeProtocols], notificationChannels: [...template.draft.notificationChannels] });
}; };
const severityColor = (severity: AlertRule['severity']) => severity === 'critical' ? 'red' : severity === 'major' ? 'orange' : 'amber'; const severityColor = (severity: AlertRule['severity']) => severity === 'critical' ? 'red' : severity === 'major' ? 'orange' : 'amber';
return <div className="v2-alert-rules"> const toggleButton = draft.version ? <Button
<Card className="v2-alert-rule-list" bodyStyle={{ padding: 0 }}> htmlType="button"
<WorkspacePanelHeader theme="light"
className="v2-alert-rule-list-heading" disabled={toggle.isPending || save.isPending}
title="规则配置" onClick={() => {
description={rules.length ? `${rules.length} 条可审计规则` : '尚未创建规则'} const current = rules.find((item) => item.id === draft.id);
actions={<Button theme="light" type="primary" icon={<IconPlus />} onClick={() => { setSelectedID('__new__'); setDraft(emptyRule()); }}></Button>} if (current) toggle.mutate(current);
/> }}
<div className="v2-alert-rule-items" role="region" tabIndex={0} aria-label="告警规则列表,可左右滚动选择"> >{toggle.isPending ? '更新中' : draft.enabled ? '停用规则' : '启用规则'}</Button> : null;
{rules.map((rule) => <Button const editor = <Card className="v2-alert-rule-editor" bodyStyle={{ padding: 0 }}>
className={`v2-alert-rule-item${selectedID === rule.id ? ' is-selected' : ''}`}
key={rule.id}
theme="borderless"
type="tertiary"
aria-pressed={selectedID === rule.id}
onClick={() => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); }}
>
<span className={`v2-alert-rule-dot is-${rule.severity}`} aria-hidden="true" />
<span className="v2-alert-rule-copy">
<span><strong>{rule.name}</strong><Tag size="small" color={severityColor(rule.severity)} type="light">{severityLabels[rule.severity]}</Tag></span>
<small>{ruleCondition(rule, catalogLabels)} · v{rule.version}</small>
</span>
<Tag className="v2-alert-rule-state" size="small" color={rule.enabled ? 'green' : 'grey'} type="light">{rule.enabled ? '已启用' : '已停用'}</Tag>
</Button>)}
{!rules.length ? <Empty className="v2-alert-rule-empty" title="暂无告警规则" description="从右侧模板快速创建,或新建一条自定义规则。" /> : null}
</div>
</Card>
<Card className="v2-alert-rule-editor" bodyStyle={{ padding: 0 }}>
<form className="v2-alert-rule-editor-form" onSubmit={(event) => { event.preventDefault(); save.mutate(draft); }}> <form className="v2-alert-rule-editor-form" onSubmit={(event) => { event.preventDefault(); save.mutate(draft); }}>
<WorkspacePanelHeader {!mobileLayout ? <WorkspacePanelHeader
className="v2-alert-rule-editor-heading" className="v2-alert-rule-editor-heading"
title={draft.version ? '编辑规则' : '新建规则'} title={draft.version ? '编辑规则' : '新建规则'}
description="数值、状态与主数据范围均纳入版本审计" description="数值、状态与主数据范围均纳入版本审计"
actions={draft.version ? <Button htmlType="button" theme="light" disabled={toggle.isPending || save.isPending} onClick={() => { const current = rules.find((item) => item.id === draft.id); if (current) toggle.mutate(current); }}>{toggle.isPending ? '更新中' : draft.enabled ? '停用规则' : '启用规则'}</Button> : null} actions={toggleButton}
/> /> : <div className={`v2-alert-rule-sheet-status${draft.version ? '' : ' is-new'}`}>
<span>
<Tag size="small" color={draft.version ? draft.enabled ? 'green' : 'grey' : 'blue'} type="light">{draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'}</Tag>
<small>{draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'}</small>
</span>
{toggleButton}
</div>}
<section className="v2-alert-rule-templates" aria-label="告警规则模板"> <section className="v2-alert-rule-templates" aria-label="告警规则模板">
<div><strong></strong><span></span></div> <div><strong></strong><span></span></div>
<nav>{RULE_TEMPLATES.map((template) => { <nav>{RULE_TEMPLATES.map((template) => {
@@ -398,7 +399,48 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
</div> </div>
<footer><div><b></b><span> / </span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer> <footer><div><b></b><span> / </span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer>
</form> </form>
</Card>;
return <div className={`v2-alert-rules${mobileLayout ? ' is-mobile' : ''}`}>
<Card className="v2-alert-rule-list" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
className="v2-alert-rule-list-heading"
title="规则配置"
description={rules.length ? `${rules.length} 条可审计规则` : '尚未创建规则'}
actions={<Button theme="light" type="primary" icon={<IconPlus />} aria-haspopup={mobileLayout ? 'dialog' : undefined} aria-expanded={mobileLayout ? editorOpen && selectedID === '__new__' : undefined} onClick={openNewRule}></Button>}
/>
<div className="v2-alert-rule-items" role="region" tabIndex={0} aria-label={mobileLayout ? '告警规则列表' : '告警规则列表,可左右滚动选择'}>
{rules.map((rule) => <Button
className={`v2-alert-rule-item${selectedID === rule.id ? ' is-selected' : ''}`}
key={rule.id}
theme="borderless"
type="tertiary"
aria-pressed={selectedID === rule.id}
aria-haspopup={mobileLayout ? 'dialog' : undefined}
aria-expanded={mobileLayout ? editorOpen && selectedID === rule.id : undefined}
onClick={() => openRule(rule)}
>
<span className={`v2-alert-rule-dot is-${rule.severity}`} aria-hidden="true" />
<span className="v2-alert-rule-copy">
<span><strong>{rule.name}</strong><Tag size="small" color={severityColor(rule.severity)} type="light">{severityLabels[rule.severity]}</Tag></span>
<small>{ruleCondition(rule, catalogLabels)} · v{rule.version}</small>
</span>
<Tag className="v2-alert-rule-state" size="small" color={rule.enabled ? 'green' : 'grey'} type="light">{rule.enabled ? '已启用' : '已停用'}</Tag>
</Button>)}
{!rules.length ? <Empty className="v2-alert-rule-empty" title="暂无告警规则" description="从右侧模板快速创建,或新建一条自定义规则。" /> : null}
</div>
</Card> </Card>
{!mobileLayout ? editor : null}
{mobileLayout ? <SideSheet
className="v2-alert-rule-editor-sidesheet"
visible={editorOpen}
aria-label="告警规则编辑"
placement="bottom"
height="min(92dvh, 820px)"
title={<div className="v2-alert-sheet-title"><strong>{draft.version ? '编辑规则' : '新建规则'}</strong><span>{draft.name || '配置触发条件、范围与通知策略'}</span></div>}
onCancel={() => setEditorOpen(false)}
>
{editorOpen ? editor : null}
</SideSheet> : null}
</div>; </div>;
} }

View File

@@ -169,6 +169,7 @@ describe('V2 production entry', () => {
expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"'); expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"');
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"'); expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"');
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-detail-sidesheet"'); expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-detail-sidesheet"');
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-rule-editor-sidesheet"');
expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table'); expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table');
expect(corePageSources.AlertsPage).toContain('detailTriggerRow({'); expect(corePageSources.AlertsPage).toContain('detailTriggerRow({');
expect(corePageSources.AlertsPage).not.toContain('<table><thead><tr><th />'); expect(corePageSources.AlertsPage).not.toContain('<table><thead><tr><th />');

View File

@@ -1592,6 +1592,7 @@
.v2-alert-filter-sidesheet .semi-sidesheet-body, .v2-alert-filter-sidesheet .semi-sidesheet-body,
.v2-alert-detail-sidesheet .semi-sidesheet-body, .v2-alert-detail-sidesheet .semi-sidesheet-body,
.v2-alert-rule-editor-sidesheet .semi-sidesheet-body,
.v2-access-detail-sidesheet .semi-sidesheet-body, .v2-access-detail-sidesheet .semi-sidesheet-body,
.v2-access-governance-sidesheet .semi-sidesheet-body { .v2-access-governance-sidesheet .semi-sidesheet-body {
background: #f6f8fb; background: #f6f8fb;
@@ -7562,6 +7563,21 @@
overflow: hidden; overflow: hidden;
} }
.v2-alert-rule-editor-sidesheet .semi-sidesheet-body {
display: flex;
min-height: 0;
overflow: hidden;
padding: 0;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor.semi-card {
width: 100%;
height: 100%;
border: 0;
border-radius: 0;
box-shadow: none;
}
.v2-alert-rule-list > .semi-card-body, .v2-alert-rule-list > .semi-card-body,
.v2-alert-rule-editor > .semi-card-body, .v2-alert-rule-editor > .semi-card-body,
.v2-alert-rule-editor-form { .v2-alert-rule-editor-form {
@@ -7587,6 +7603,7 @@
.v2-alert-rule-editor-form { .v2-alert-rule-editor-form {
overflow: auto; overflow: auto;
overscroll-behavior: contain;
} }
.v2-alert-rule-list > .semi-card-body > .v2-alert-rule-list-heading, .v2-alert-rule-list > .semi-card-body > .v2-alert-rule-list-heading,
@@ -7604,6 +7621,38 @@
margin-top: 6px; margin-top: 6px;
} }
.v2-alert-rule-sheet-status {
display: flex;
min-height: 56px;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid #e3e9f1;
background: #fff;
padding: 9px 14px;
}
.v2-alert-rule-sheet-status > span {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
}
.v2-alert-rule-sheet-status small {
overflow: hidden;
color: #7c899b;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-alert-rule-sheet-status .semi-button {
min-width: 82px;
flex: 0 0 auto;
}
.v2-alert-rule-templates { .v2-alert-rule-templates {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
align-items: stretch; align-items: stretch;
@@ -7668,6 +7717,10 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.v2-alert-rule-editor-form > footer .v2-primary-button .semi-button-content {
color: #fff;
}
.v2-alert-notifications.semi-card { .v2-alert-notifications.semi-card {
min-height: 0; min-height: 0;
flex: 1 1 0; flex: 1 1 0;
@@ -8093,46 +8146,146 @@
height: 32px; height: 32px;
} }
.v2-alert-page.is-rules {
height: 100%;
min-height: 0;
overflow: hidden;
padding-bottom: 8px;
}
.v2-alert-rules { .v2-alert-rules {
display: flex; display: flex;
flex: none;
flex-direction: column; flex-direction: column;
overflow: visible;
} }
.v2-alert-rule-list { .v2-alert-rules.is-mobile {
max-height: 270px; min-height: 0;
flex: 1 1 0;
overflow: hidden;
} }
.v2-alert-rule-items { .v2-alert-rules.is-mobile .v2-alert-rule-list {
min-height: 0;
max-height: none;
flex: 1 1 0;
}
.v2-alert-rules.is-mobile .v2-alert-rule-items {
display: flex;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
gap: 8px;
padding: 8px;
scrollbar-color: #9dacc0 transparent; scrollbar-color: #9dacc0 transparent;
scrollbar-width: thin; scrollbar-width: thin;
} }
.v2-alert-rule-items:focus-visible { .v2-alert-rules.is-mobile .v2-alert-rule-items:focus-visible {
outline: 2px solid rgba(18, 104, 243, .34); outline: 2px solid rgba(18, 104, 243, .34);
outline-offset: -2px; outline-offset: -2px;
} }
.v2-alert-rule-items::-webkit-scrollbar { .v2-alert-rules.is-mobile .v2-alert-rule-items::-webkit-scrollbar {
display: block; display: block;
height: 8px; width: 8px;
} }
.v2-alert-rule-items::-webkit-scrollbar-thumb { .v2-alert-rules.is-mobile .v2-alert-rule-items::-webkit-scrollbar-thumb {
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 999px; border-radius: 999px;
background: #9dacc0; background: #9dacc0;
background-clip: padding-box; background-clip: padding-box;
} }
.v2-alert-rule-editor.semi-card, .v2-alert-rules.is-mobile .v2-alert-rule-item.semi-button {
.v2-alert-rule-editor-form { width: 100%;
overflow: visible; min-width: 0;
min-height: 76px;
flex: 0 0 auto;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
box-shadow: 0 2px 8px rgba(36, 53, 76, .04);
} }
.v2-alert-rule-editor-form > footer { .v2-alert-rules.is-mobile .v2-alert-rule-item.semi-button.is-selected {
position: static; border-color: rgba(18, 104, 243, .38);
background: linear-gradient(135deg, rgba(18, 104, 243, .075), rgba(255, 255, 255, .98));
box-shadow: 0 5px 16px rgba(18, 104, 243, .1);
}
.v2-alert-rule-editor-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner {
width: 100% !important;
max-width: 100%;
overflow: hidden;
border-radius: 18px 18px 0 0;
box-shadow: 0 -18px 54px rgba(22, 34, 50, .22);
}
.v2-alert-rule-editor-sidesheet.semi-sidesheet-bottom .semi-sidesheet-header {
min-height: 68px;
border-bottom: 1px solid #dfe6ef;
padding: 13px 15px 11px;
}
.v2-alert-rule-editor-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body {
min-height: 0;
overflow: hidden;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor-form {
overflow-y: auto;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-templates {
padding: 12px;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-templates > nav {
display: flex;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
}
.v2-alert-rule-editor-sidesheet .v2-alert-template-card.semi-button {
min-width: min(294px, calc(100vw - 40px));
scroll-snap-align: start;
}
.v2-alert-rule-editor-sidesheet .v2-rule-form-grid {
padding: 14px 14px 18px;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor-form > footer {
display: grid;
position: sticky;
min-height: 68px;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
flex-direction: row;
gap: 10px;
padding: 9px 14px calc(9px + env(safe-area-inset-bottom));
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor-form > footer > div {
overflow: hidden;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor-form > footer span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-alert-rule-editor-sidesheet .v2-alert-rule-editor-form > footer > .semi-button {
width: auto;
min-width: 104px;
height: 42px;
justify-content: center;
} }
.v2-alert-notifications.semi-card { .v2-alert-notifications.semi-card {