mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +00:00
feat: add notification suppression rules (#1525)
* feat: add notification suppression rules * fix: restore label routing and routing test mocks for suppression * fix: allow bell mute shortcuts for history-only notification categories Suppression rule validation used the routable category whitelist, which rejected history-only categories such as update_started that appear in the bell during stack updates. * feat: expand Mute Rules UX with compose-first entry points and activity badges * fix: add missing NodeContext mocks for notification suppression tests
This commit is contained in:
@@ -14,6 +14,9 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { NodeMuteSubmenu } from '@/components/mute/MuteMenuItems';
|
||||
import { useNodeMuteActions } from '@/hooks/useMuteRuleActions';
|
||||
import type { MuteRuleDraft } from '@/lib/muteRules';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
@@ -42,6 +45,7 @@ export interface NodeCardProps {
|
||||
onCordonChange?: () => void;
|
||||
onEdit?: (node: Node) => void;
|
||||
onDelete?: (node: Node) => void;
|
||||
onOpenMuteRulesWithPrefill?: (draft: MuteRuleDraft) => void;
|
||||
}
|
||||
|
||||
// --- Sub-Components ---
|
||||
@@ -59,7 +63,7 @@ function UsageBar({ percent, color }: { percent: number; color: string }) {
|
||||
|
||||
// --- Main Export ---
|
||||
|
||||
export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate, onCordonChange, onEdit, onDelete }: NodeCardProps) {
|
||||
export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate, onCordonChange, onEdit, onDelete, onOpenMuteRulesWithPrefill }: NodeCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [stacks, setStacks] = useState<string[] | null>(node.stacks);
|
||||
const [loadingStacks, setLoadingStacks] = useState(false);
|
||||
@@ -77,7 +81,12 @@ export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, u
|
||||
// (requirePermission('node:manage','node',id) + requirePaid). Gating on tier
|
||||
// alone would surface the control to deployer/viewer/auditor users whose calls 403.
|
||||
const canCordon = isPaid && can('node:manage', 'node', String(node.id));
|
||||
const showMenu = canEdit || canDelete || canCordon;
|
||||
const nodeMuteActions = useNodeMuteActions(
|
||||
node.id,
|
||||
node.name,
|
||||
onOpenMuteRulesWithPrefill ?? (() => {}),
|
||||
);
|
||||
const showMenu = canEdit || canDelete || canCordon || (nodeMuteActions.canMute && Boolean(onOpenMuteRulesWithPrefill));
|
||||
|
||||
const isOnline = node.status === 'online';
|
||||
const isLocal = node.type === 'local';
|
||||
@@ -182,6 +191,7 @@ export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, u
|
||||
{node.cordoned ? 'Uncordon node' : 'Cordon node'}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onOpenMuteRulesWithPrefill && <NodeMuteSubmenu actions={nodeMuteActions} />}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { FleetTopologyNode, LayoutMode, SavedPositions } from '@/lib/fleet-
|
||||
import type { Label as StackLabel } from '../label-types';
|
||||
import type { Node } from '@/context/NodeContext';
|
||||
import type { FleetNode, NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './types';
|
||||
import type { MuteRuleDraft } from '@/lib/muteRules';
|
||||
|
||||
interface OverviewTabProps {
|
||||
loading: boolean;
|
||||
@@ -35,6 +36,7 @@ interface OverviewTabProps {
|
||||
onCordonChange?: () => void;
|
||||
onEditNode?: (node: Node) => void;
|
||||
onDeleteNode?: (node: Node) => void;
|
||||
onOpenMuteRulesWithPrefill?: (draft: MuteRuleDraft) => void;
|
||||
topologyMode: LayoutMode;
|
||||
onTopologyModeChange: (mode: LayoutMode) => void;
|
||||
topologyPositions: SavedPositions;
|
||||
@@ -70,6 +72,7 @@ export function OverviewTab({
|
||||
onCordonChange,
|
||||
onEditNode,
|
||||
onDeleteNode,
|
||||
onOpenMuteRulesWithPrefill,
|
||||
topologyMode,
|
||||
onTopologyModeChange,
|
||||
topologyPositions,
|
||||
@@ -149,6 +152,7 @@ export function OverviewTab({
|
||||
onCordonChange={onCordonChange}
|
||||
onEdit={onEditNode}
|
||||
onDelete={onDeleteNode}
|
||||
onOpenMuteRulesWithPrefill={onOpenMuteRulesWithPrefill}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ function baseProps(node: FleetNode) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useNodesMock.mockReturnValue({ nodes: [] });
|
||||
useNodesMock.mockReturnValue({ nodes: [], hasCapability: vi.fn(() => false) });
|
||||
useAuthMock.mockReturnValue({ isAdmin: true, can: vi.fn(() => true) });
|
||||
useLicenseMock.mockReturnValue({ isPaid: false });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user