mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 18:44:53 +00:00
Register filter button group governance
This commit is contained in:
parent
36af6620d3
commit
23e65040cc
9 changed files with 215 additions and 37 deletions
|
|
@ -1164,6 +1164,7 @@ the canonical monitored-system blocked payload.
|
|||
and the Patrol runtime-remediation destination shared with the AI settings endpoint, so summary actions, run-history runtime-failure actions, and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination to Patrol operators as Patrol provider configuration instead of generic `AI Settings` copy
|
||||
and the Patrol route-shell destination itself, so the thin page shell at `frontend-modern/src/pages/AIIntelligence.tsx` may continue to bridge the shared AI-runtime payload boundary while exposing `/patrol` as the canonical product route and keeping retired `/ai` browser entry points unregistered
|
||||
and the Patrol route-shell accessibility boundary, so brand icons in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx` stay decorative when the same heading already exposes visible Patrol text, preventing duplicate accessible names such as `Pulse Patrol Patrol`
|
||||
and the Patrol autonomy selector boundary, so the header composes the shared `frontend-modern/src/components/shared/FilterButtonGroup.tsx` segmented primitive for the `Monitor` / `Investigate` / `Remediate` presentation while the API contract remains the sole owner of accepted autonomy values, license-required rejection shape, and the monitor-only clamp
|
||||
9. Route frontend API-client parsed error propagation, API-error-status fallback handling, allowed-status handling, custom status-specific error handling, command-trigger success envelope handling, shared response parsing pipelines, missing-resource lookup handling, metadata CRUD routing, stream event consumption, response status, collection normalization, scalar payload coercion, and structured error normalization through canonical shared helpers under `frontend-modern/src/api/`
|
||||
Assistant chat stream workflow-state payloads are part of this same
|
||||
frontend API-client boundary. `workflow_state` events must keep `phase`,
|
||||
|
|
@ -5334,6 +5335,12 @@ owners must not rely on that 402 as normal control flow for stale local state:
|
|||
when the current entitlement locks safe remediation, they submit `monitor` even
|
||||
if older persisted settings or a previous entitlement left `approval`,
|
||||
`assisted`, or `full` in memory.
|
||||
The Patrol header presentation for that API boundary must compose the shared
|
||||
`frontend-modern/src/components/shared/FilterButtonGroup.tsx` segmented
|
||||
selector: the endpoint contract still owns the accepted autonomy values and
|
||||
license-required response shape, while the frontend owns only the option
|
||||
mapping and entitlement-derived disabled state. Local Patrol selector styling
|
||||
must not become a second source of truth for the API's monitor-only clamp.
|
||||
The reporting transport contract now also carries an optional narrative
|
||||
interpretation layer alongside the deterministic data surface. The Go-side
|
||||
`pkg/reporting.MetricReportRequest` gains optional `Narrator` and
|
||||
|
|
|
|||
|
|
@ -2416,6 +2416,12 @@ small leading status-dot glyph used by filter options. Page and feature
|
|||
surfaces must not copy the chip-dot `<span>` factory or import the legacy
|
||||
`PageControls` deck for resource-list filtering; those drift checks live in
|
||||
`shared-template-registry.json` and run through `shared-template-audit.mjs`.
|
||||
Segmented selectors are registry-backed too. `FilterButtonGroup` owns the
|
||||
settings, prominent, compact, and equal segmented selector shells, including
|
||||
active-button tone, disabled-option behavior, pressed-state semantics,
|
||||
compact labels, and horizontal scroll treatment through the shared
|
||||
shell/state/model split. Settings and compact feature surfaces must compose
|
||||
that primitive instead of copying active-button selector styling locally.
|
||||
Chart visibility display actions are registry-backed too.
|
||||
`ChartVisibilityToggleButton` owns the `Show charts` / `Hide charts` label,
|
||||
pressed-state, title, icon, and toolbar action styling for summary-bearing
|
||||
|
|
|
|||
|
|
@ -597,6 +597,13 @@ surface must render any Patrol readiness
|
|||
provider, model, and summary carried by the failure object, and keep the
|
||||
provider-settings action immediately available before the operator chooses to
|
||||
open the Assistant handoff.
|
||||
The Patrol autonomy selector in that header must compose the shared
|
||||
`frontend-modern/src/components/shared/FilterButtonGroup.tsx` segmented
|
||||
variant instead of rebuilding a local active-button group. Patrol owns only
|
||||
the domain options (`Monitor`, `Investigate`, `Remediate`), entitlement locks,
|
||||
and the `full`-as-remediate presentation mapping; the shared primitive owns
|
||||
the equal-width segmented shell, pressed-state semantics, disabled-option
|
||||
behavior, and active/inactive selector styling.
|
||||
That same Patrol-owned presentation rule also applies to the findings empty
|
||||
state: `frontend-modern/src/components/AI/FindingsPanel.tsx` must not treat
|
||||
`0 active findings` as equivalent to "your infrastructure looks healthy" when
|
||||
|
|
|
|||
|
|
@ -240,6 +240,25 @@
|
|||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "filter-button-group-shell",
|
||||
"category": "segmented-selector",
|
||||
"summary": "Settings and compact feature segmented selectors must compose FilterButtonGroup instead of recreating active-button group styling locally.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/FilterButtonGroup.tsx",
|
||||
"export": "FilterButtonGroup"
|
||||
},
|
||||
"requiredConsumers": [
|
||||
{ "path": "src/components/Settings/GeneralSettingsPanel.tsx" },
|
||||
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
||||
{ "path": "src/features/patrol/PatrolIntelligenceHeader.tsx" }
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/FilterButtonGroup.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chart-visibility-toggle-button",
|
||||
"category": "display-toggle",
|
||||
|
|
@ -517,6 +536,27 @@
|
|||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "filter-button-group-local-segmented-control-styles",
|
||||
"category": "segmented-selector",
|
||||
"summary": "Settings and feature surfaces must not copy the compact active-button segmented-control styling owned by FilterButtonGroup.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/filterButtonGroupModel.ts",
|
||||
"export": "getFilterButtonGroupButtonClass"
|
||||
},
|
||||
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"flex items-center bg-base rounded-md p-1 border shadow-inner",
|
||||
"flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. Settings and compact feature segmented selectors must use FilterButtonGroup; local active-button selector styling forks are not allowed.",
|
||||
"allowedPaths": [],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"requiredPatternGuards": [
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ describe('FilterButtonGroup', () => {
|
|||
expect(filterButtonGroupModelSource).toContain('getFilterButtonGroupButtonClass');
|
||||
expect(filterButtonGroupModelSource).toContain('getFilterButtonGroupCompactLabel');
|
||||
expect(filterButtonGroupModelSource).toContain("prominent: 'grid grid-cols-1 gap-2'");
|
||||
expect(filterButtonGroupModelSource).toContain('segmented:');
|
||||
expect(filterButtonGroupModelSource).toContain('compact:');
|
||||
expect(filterButtonGroupModelSource).toContain('inline-flex items-center gap-1');
|
||||
});
|
||||
|
|
@ -168,4 +169,30 @@ describe('FilterButtonGroup', () => {
|
|||
expect(visualLabel).toHaveClass('inline-flex');
|
||||
expect(visualLabel).toHaveClass('whitespace-nowrap');
|
||||
});
|
||||
|
||||
it('supports equal-width segmented controls for compact feature settings', () => {
|
||||
render(() => (
|
||||
<FilterButtonGroup
|
||||
ariaLabel="Patrol autonomy level"
|
||||
options={[
|
||||
{ value: 'monitor', label: 'Monitor' },
|
||||
{ value: 'approval', label: 'Investigate', disabled: true },
|
||||
{ value: 'assisted', label: 'Remediate', disabled: true },
|
||||
]}
|
||||
value="monitor"
|
||||
onChange={() => undefined}
|
||||
variant="segmented"
|
||||
/>
|
||||
));
|
||||
|
||||
const group = screen.getByRole('group', { name: 'Patrol autonomy level' });
|
||||
const activeButton = within(group).getByRole('button', { name: 'Monitor' });
|
||||
const disabledButton = within(group).getByRole('button', { name: 'Investigate' });
|
||||
|
||||
expect(activeButton.className).toContain('flex-1');
|
||||
expect(activeButton.className).toContain('bg-surface');
|
||||
expect(activeButton.className).toContain('text-blue-600');
|
||||
expect(disabledButton).toBeDisabled();
|
||||
expect(disabledButton.className).toContain('opacity-50');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ import reportingPanelSource from '@/components/Settings/ReportingPanel.tsx?raw';
|
|||
import rolesPanelSource from '@/components/Settings/RolesPanel.tsx?raw';
|
||||
import updatesSettingsPanelSource from '@/components/Settings/UpdatesSettingsPanel.tsx?raw';
|
||||
import userAssignmentsPanelSource from '@/components/Settings/UserAssignmentsPanel.tsx?raw';
|
||||
import patrolIntelligenceHeaderSource from '@/features/patrol/PatrolIntelligenceHeader.tsx?raw';
|
||||
import filterBarSource from '@/components/shared/FilterBar/FilterBar.tsx?raw';
|
||||
import filterChipSource from '@/components/shared/FilterBar/FilterChip.tsx?raw';
|
||||
import addFilterMenuSource from '@/components/shared/FilterBar/AddFilterMenu.tsx?raw';
|
||||
|
|
@ -187,7 +188,54 @@ describe('shared primitive guardrails', () => {
|
|||
expect(rawTableUsers).toEqual(['./PulseDataGrid.tsx']);
|
||||
});
|
||||
|
||||
it('routes canonical settings segmented selectors through FilterButtonGroup', () => {
|
||||
it('routes canonical settings and feature segmented selectors through FilterButtonGroup', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
requiredConsumers?: Array<{ path?: string }>;
|
||||
}>;
|
||||
patternGuards?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
allPatterns?: string[];
|
||||
scopes?: string[];
|
||||
allowedPaths?: string[];
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules?.find(
|
||||
(rule) => rule.id === 'filter-button-group-shell',
|
||||
);
|
||||
const registeredGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'filter-button-group-local-segmented-control-styles',
|
||||
);
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe(
|
||||
'src/components/shared/FilterButtonGroup.tsx',
|
||||
);
|
||||
expect(registeredRule?.canonical?.export).toBe('FilterButtonGroup');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
|
||||
'src/components/Settings/GeneralSettingsPanel.tsx',
|
||||
'src/components/Settings/ReportingPanel.tsx',
|
||||
'src/features/patrol/PatrolIntelligenceHeader.tsx',
|
||||
]);
|
||||
expect(registeredGuard?.canonical?.path).toBe(
|
||||
'src/components/shared/filterButtonGroupModel.ts',
|
||||
);
|
||||
expect(registeredGuard?.canonical?.export).toBe('getFilterButtonGroupButtonClass');
|
||||
expect(registeredGuard?.allPatterns).toEqual([
|
||||
'flex items-center bg-base rounded-md p-1 border shadow-inner',
|
||||
'flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200',
|
||||
]);
|
||||
expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(registeredGuard?.scopes).toEqual(
|
||||
expect.arrayContaining([
|
||||
'src/components/Settings',
|
||||
'src/features',
|
||||
'src/pages',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(filterButtonGroupSource).toContain('useFilterButtonGroupState');
|
||||
expect(filterButtonGroupSource).toContain('getFilterButtonGroupClass');
|
||||
expect(filterButtonGroupSource).toContain('getFilterButtonGroupButtonClass');
|
||||
|
|
@ -200,6 +248,7 @@ describe('shared primitive guardrails', () => {
|
|||
expect(filterButtonGroupStateSource).toContain('props.disabled || option.disabled');
|
||||
expect(filterButtonGroupStateSource).toContain('props.onChange(option.value)');
|
||||
expect(filterButtonGroupModelSource).toContain("prominent: 'grid grid-cols-1 gap-2'");
|
||||
expect(filterButtonGroupModelSource).toContain('segmented:');
|
||||
expect(filterButtonGroupModelSource).toContain('touch-scroll');
|
||||
expect(filterButtonGroupModelSource).toContain('getFilterButtonGroupButtonClass');
|
||||
expect(filterButtonGroupModelSource).toContain('getFilterButtonGroupCompactLabel');
|
||||
|
|
@ -215,6 +264,15 @@ describe('shared primitive guardrails', () => {
|
|||
expect(reportingPanelSource).toContain('variant="prominent"');
|
||||
expect(reportingPanelSource).not.toContain('getReportingToggleButtonClass');
|
||||
expect(reportingPanelSource).not.toContain('<For each={REPORTING_RANGE_OPTIONS}>');
|
||||
expect(patrolIntelligenceHeaderSource).toContain('FilterButtonGroup');
|
||||
expect(patrolIntelligenceHeaderSource).toContain('variant="segmented"');
|
||||
expect(patrolIntelligenceHeaderSource).toContain('selectedAutonomyLevel');
|
||||
expect(patrolIntelligenceHeaderSource).not.toContain(
|
||||
'flex items-center bg-base rounded-md p-1 border shadow-inner',
|
||||
);
|
||||
expect(patrolIntelligenceHeaderSource).not.toContain(
|
||||
'flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps AI model picker labels route-aware for gateway providers', () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ export interface FilterOption<T extends string | number> {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type FilterButtonGroupVariant = 'default' | 'settings' | 'compact' | 'prominent';
|
||||
export type FilterButtonGroupVariant =
|
||||
| 'default'
|
||||
| 'settings'
|
||||
| 'compact'
|
||||
| 'prominent'
|
||||
| 'segmented';
|
||||
export type FilterButtonGroupOptionTone =
|
||||
| 'default'
|
||||
| 'info'
|
||||
|
|
@ -39,6 +44,8 @@ const groupClassByVariant: Record<FilterButtonGroupVariant, string> = {
|
|||
compact:
|
||||
'inline-flex items-center gap-1 bg-surface-hover rounded-md p-0.5 ring-1 ring-border-subtle overflow-x-auto scrollbar-hide',
|
||||
prominent: 'grid grid-cols-1 gap-2',
|
||||
segmented:
|
||||
'flex items-center gap-1 rounded-md border border-border bg-base p-1 shadow-inner overflow-x-auto scrollbar-hide',
|
||||
};
|
||||
|
||||
const labelClassByVariant: Partial<Record<FilterButtonGroupVariant, string>> = {
|
||||
|
|
@ -109,6 +116,16 @@ export function getFilterButtonGroupButtonClass(
|
|||
].join(' ');
|
||||
}
|
||||
|
||||
if (variant === 'segmented') {
|
||||
return [
|
||||
'flex-1 min-h-8 px-2 py-1.5 text-xs font-semibold rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
|
||||
active
|
||||
? 'bg-surface text-blue-600 shadow-sm dark:text-blue-400'
|
||||
: 'text-muted hover:text-base-content hover:bg-surface-hover',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : '',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
return [
|
||||
'flex flex-1 justify-center sm:flex-none sm:justify-start items-center gap-2 px-3 sm:px-4 py-2.5 sm:py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
|
||||
active
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { PageHeader } from '@/components/shared/PageHeader';
|
|||
import { Toggle, TogglePrimitive } from '@/components/shared/Toggle';
|
||||
import { CountdownTimer } from '@/components/patrol';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup';
|
||||
import type { PatrolAutonomyLevel } from '@/api/patrol';
|
||||
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { groupModelsByProvider } from '@/utils/patrolFormat';
|
||||
|
|
@ -21,6 +23,8 @@ import { PATROL_PROVIDER_SETTINGS_ACTION } from '@/utils/patrolRuntimeActions';
|
|||
import type { PatrolConfigurationFailureInput } from './patrolInvestigationContextModel';
|
||||
import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
|
||||
|
||||
type PatrolSelectableAutonomyLevel = Exclude<PatrolAutonomyLevel, 'full'>;
|
||||
|
||||
const isNonEmptyConfigurationDetail = (value?: string | null): value is string =>
|
||||
Boolean(value?.trim());
|
||||
|
||||
|
|
@ -73,6 +77,34 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
if (!model || models.length === 0) return false;
|
||||
return !models.some((candidate) => candidate.id === model);
|
||||
});
|
||||
const selectedAutonomyLevel = createMemo<PatrolSelectableAutonomyLevel>(() => {
|
||||
const level = state.autonomyLevel();
|
||||
return level === 'full' ? 'assisted' : level;
|
||||
});
|
||||
const getAutonomyOptionTitle = (level: PatrolSelectableAutonomyLevel) => {
|
||||
const isProLocked = () => state.autoFixLocked() && level !== 'monitor';
|
||||
if (!presentationPolicyHidesUpgradePrompts() && isProLocked()) {
|
||||
return level === 'approval'
|
||||
? 'Investigation is not enabled on this plan'
|
||||
: 'Safe remediation workflows are not enabled on this plan';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const autonomyLevelOptions = createMemo<FilterOption<PatrolSelectableAutonomyLevel>[]>(() => [
|
||||
{ value: 'monitor', label: 'Monitor' },
|
||||
{
|
||||
value: 'approval',
|
||||
label: 'Investigate',
|
||||
disabled: state.autoFixLocked(),
|
||||
title: getAutonomyOptionTitle('approval'),
|
||||
},
|
||||
{
|
||||
value: 'assisted',
|
||||
label: 'Remediate',
|
||||
disabled: state.autoFixLocked(),
|
||||
title: getAutonomyOptionTitle('assisted'),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
|
|
@ -288,41 +320,15 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center bg-base rounded-md p-1 border shadow-inner">
|
||||
<For each={['monitor', 'approval', 'assisted'] as const}>
|
||||
{(level) => {
|
||||
const isProLocked = () =>
|
||||
state.autoFixLocked() && (level === 'approval' || level === 'assisted');
|
||||
const isDisabled = () => !state.patrolEnabledLocal() || isProLocked();
|
||||
const isActive = () =>
|
||||
level === 'assisted'
|
||||
? state.autonomyLevel() === 'assisted' ||
|
||||
state.autonomyLevel() === 'full'
|
||||
: state.autonomyLevel() === level;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => state.handleAutonomyChange(level)}
|
||||
disabled={isDisabled()}
|
||||
title={
|
||||
!presentationPolicyHidesUpgradePrompts() && isProLocked()
|
||||
? level === 'approval'
|
||||
? 'Investigation is not enabled on this plan'
|
||||
: 'Safe remediation workflows are not enabled on this plan'
|
||||
: undefined
|
||||
}
|
||||
class={`flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200 ${isActive() ? ' text-blue-600 dark:text-blue-400 shadow-[0_1px_3px_rgba(0,0,0,0.1)]' : isDisabled() ? ' ' : 'text-muted hover:text-base-content hover:bg-surface-hover'} ${isDisabled() ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{level === 'monitor'
|
||||
? 'Monitor'
|
||||
: level === 'approval'
|
||||
? 'Investigate'
|
||||
: 'Remediate'}
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<FilterButtonGroup
|
||||
ariaLabel="Patrol autonomy level"
|
||||
class="w-full"
|
||||
disabled={!state.patrolEnabledLocal()}
|
||||
options={autonomyLevelOptions()}
|
||||
value={selectedAutonomyLevel()}
|
||||
onChange={(level) => state.handleAutonomyChange(level)}
|
||||
variant="segmented"
|
||||
/>
|
||||
<Show when={!presentationPolicyHidesUpgradePrompts() && state.autoFixLocked()}>
|
||||
<div class="pl-1 text-[11px] text-muted">
|
||||
Investigation and safe remediation workflows are not enabled on this plan.
|
||||
|
|
|
|||
|
|
@ -3542,6 +3542,16 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(patrolIntelligenceHeaderSource).toContain(
|
||||
'!presentationPolicyHidesUpgradePrompts() && state.alertAnalysisLocked()',
|
||||
);
|
||||
expect(patrolIntelligenceHeaderSource).toContain(
|
||||
"from '@/components/shared/FilterButtonGroup'",
|
||||
);
|
||||
expect(patrolIntelligenceHeaderSource).toContain('variant="segmented"');
|
||||
expect(patrolIntelligenceHeaderSource).not.toContain(
|
||||
'flex items-center bg-base rounded-md p-1 border shadow-inner',
|
||||
);
|
||||
expect(patrolIntelligenceHeaderSource).not.toContain(
|
||||
'flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200',
|
||||
);
|
||||
expect(appSource).not.toContain("import('./pages/Operations')");
|
||||
expect(appSource).not.toContain('<Route path="/operations/*"');
|
||||
expect(settingsNavigationModelSource).not.toContain(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue