From 8b0386868218b40dd3e238921962956d8b4ac537 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Mon, 23 Mar 2026 16:05:44 -0700 Subject: [PATCH 01/10] feat: add Seed Data developer page (ENG-3100) Add a Seed Data page under the Developer nav (dev-only) that lets users select and trigger seed scenarios via the seed API. Includes RTK Query slice with status polling and cache tag invalidation mapped per seed task. Currently supports the PBAC scenario. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/common/nav/nav-config.tsx | 6 + .../src/features/common/nav/routes.ts | 1 + .../src/features/seed-data/SeedDataPanel.tsx | 189 ++++++++++++++++++ .../src/features/seed-data/seed-data.slice.ts | 104 ++++++++++ clients/admin-ui/src/pages/poc/seed-data.tsx | 22 ++ 5 files changed, 322 insertions(+) create mode 100644 clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx create mode 100644 clients/admin-ui/src/features/seed-data/seed-data.slice.ts create mode 100644 clients/admin-ui/src/pages/poc/seed-data.tsx diff --git a/clients/admin-ui/src/features/common/nav/nav-config.tsx b/clients/admin-ui/src/features/common/nav/nav-config.tsx index aae71d7d7b..b990d574ce 100644 --- a/clients/admin-ui/src/features/common/nav/nav-config.tsx +++ b/clients/admin-ui/src/features/common/nav/nav-config.tsx @@ -373,6 +373,12 @@ if (process.env.NEXT_PUBLIC_APP_ENV === "development") { scopes: [ScopeRegistryEnum.DEVELOPER_READ], requiresPlus: true, }, + { + title: "Seed Data", + path: routes.SEED_DATA_ROUTE, + scopes: [ScopeRegistryEnum.DEVELOPER_READ], + requiresPlus: true, + }, { title: "Test Monitors", path: routes.TEST_MONITORS_ROUTE, diff --git a/clients/admin-ui/src/features/common/nav/routes.ts b/clients/admin-ui/src/features/common/nav/routes.ts index bd663541aa..83576f9a03 100644 --- a/clients/admin-ui/src/features/common/nav/routes.ts +++ b/clients/admin-ui/src/features/common/nav/routes.ts @@ -105,6 +105,7 @@ export const ERRORS_POC_ROUTE = "/poc/error"; export const TABLE_MIGRATION_POC_ROUTE = "/poc/table-migration"; export const FIDES_JS_DOCS = "/fides-js-docs"; export const PROMPT_EXPLORER_ROUTE = "/poc/prompt-explorer"; +export const SEED_DATA_ROUTE = "/poc/seed-data"; export const TEST_MONITORS_ROUTE = "/poc/test-monitors"; // Sandbox routes diff --git a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx new file mode 100644 index 0000000000..a861bf09f7 --- /dev/null +++ b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx @@ -0,0 +1,189 @@ +import { + Alert, + Button, + Card, + Checkbox, + Flex, + Progress, + Space, + Tag, + Typography, +} from "fidesui"; +import { useCallback, useEffect, useState } from "react"; + +import { + SeedStepStatus, + SeedTasksConfig, + useGetSeedStatusQuery, + useTriggerSeedMutation, +} from "~/features/seed-data/seed-data.slice"; + +const { Paragraph, Text } = Typography; + +interface SeedScenario { + key: keyof SeedTasksConfig; + label: string; + description: string; +} + +const SEED_SCENARIOS: SeedScenario[] = [ + { + key: "pbac", + label: "PBAC (Access Control)", + description: + "Seed data purposes, data consumers, datasets, a mock query log integration, and 60 days of access control history.", + }, +]; + +const STATUS_COLORS: Record = { + pending: "default", + in_progress: "processing", + complete: "success", + skipped: "warning", + error: "error", +}; + +const StepStatusTag = ({ status }: { status: SeedStepStatus }) => { + const color = STATUS_COLORS[status.status] ?? "default"; + const style = color === "processing" ? { color: "white" } : undefined; + return ( + + {status.status.replace("_", " ")} + + ); +}; + +const computeProgress = (steps: Record): number => { + const entries = Object.values(steps); + if (entries.length === 0) { + return 0; + } + const done = entries.filter( + (s) => + s.status === "complete" || s.status === "skipped" || s.status === "error", + ).length; + return Math.round((done / entries.length) * 100); +}; + +const SeedDataPanel = () => { + const [selectedTasks, setSelectedTasks] = useState< + Set + >(new Set()); + const [executionId, setExecutionId] = useState(null); + const [triggerSeed, { isLoading: isTriggering }] = useTriggerSeedMutation(); + + // Poll for status when an execution is in progress + const { data: statusData } = useGetSeedStatusQuery(executionId!, { + skip: !executionId, + pollingInterval: 2000, + }); + + // Stop polling when execution completes + useEffect(() => { + if ( + statusData && + (statusData.status === "complete" || statusData.status === "error") + ) { + setExecutionId(null); + } + }, [statusData]); + + const handleToggle = useCallback((key: keyof SeedTasksConfig) => { + setSelectedTasks((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }, []); + + const handleSeed = useCallback(async () => { + const tasks: SeedTasksConfig = {}; + selectedTasks.forEach((key) => { + tasks[key] = true; + }); + + try { + const result = await triggerSeed({ tasks }).unwrap(); + setExecutionId(result.execution_id); + } catch { + // Error is shown via the status polling + } + }, [selectedTasks, triggerSeed]); + + const isRunning = !!executionId || isTriggering; + const showStatus = statusData && statusData.status !== "pending"; + + return ( + + + Select which data scenarios to seed into this environment. Seeding is + idempotent — it is safe to run multiple times. + + + + {SEED_SCENARIOS.map((scenario) => ( + handleToggle(scenario.key)} + disabled={isRunning} + > + {scenario.label} +
+ {scenario.description} +
+ ))} +
+ + + + + + {showStatus ? ( +
+ + + {Object.entries(statusData.steps).map(([stepName, stepStatus]) => ( + + {stepName.replace(/_/g, " ")} + + + ))} + + {statusData.error ? ( + + ) : null} + {statusData.status === "complete" ? ( + + ) : null} +
+ ) : null} +
+ ); +}; + +export default SeedDataPanel; diff --git a/clients/admin-ui/src/features/seed-data/seed-data.slice.ts b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts new file mode 100644 index 0000000000..c9111219f4 --- /dev/null +++ b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts @@ -0,0 +1,104 @@ +import { baseApi } from "~/features/common/api.slice"; + +export interface SeedTasksConfig { + sample_resources?: boolean; + messaging_mailgun?: boolean; + storage_s3?: boolean; + linked_connections?: boolean; + consent_notices?: boolean; + consent_experiences?: boolean; + compass_vendors?: boolean; + email_templates?: boolean; + discovery_monitors?: boolean; + pbac?: boolean; +} + +export interface SeedRequest { + tasks: SeedTasksConfig; +} + +export interface SeedResponse { + execution_id: string; + message: string; +} + +export interface SeedStepStatus { + status: "pending" | "in_progress" | "complete" | "skipped" | "error"; + message?: string; + started_at?: string; + completed_at?: string; +} + +export interface SeedStatusResponse { + id: string; + status: "pending" | "in_progress" | "complete" | "error"; + started_at: string; + completed_at?: string; + steps: Record; + error?: string; +} + +/** + * Cache tags to invalidate when seeding completes. + * Maps each seed task to the tags it affects. + */ +const SEED_TASK_INVALIDATION_TAGS: Record = { + pbac: [ + "DataPurpose", + "DataConsumer", + "QueryLogConfig", + "Datastore Connection", + "Dataset", + "Datasets", + ], + sample_resources: ["System", "Dataset", "Datasets", "Policies"], + linked_connections: ["Datastore Connection"], + consent_notices: ["Privacy Notices"], + consent_experiences: ["Privacy Experience Configs"], + compass_vendors: ["System", "Dictionary", "System Vendors"], + email_templates: ["Messaging Templates"], + discovery_monitors: ["Discovery Monitor Configs", "Datastore Connection"], + messaging_mailgun: ["Messaging Config"], + storage_s3: ["Configuration Settings"], +}; + +const seedDataApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + triggerSeed: build.mutation({ + query: (body) => ({ + url: "plus/seed", + method: "POST", + body, + }), + // Invalidate tags for all enabled tasks so cached data refreshes + // after the user navigates to the seeded pages. + invalidatesTags: (_result, _error, arg) => { + const tags: string[] = []; + const { tasks } = arg; + (Object.keys(tasks) as (keyof SeedTasksConfig)[]).forEach((key) => { + if (tasks[key] && SEED_TASK_INVALIDATION_TAGS[key]) { + tags.push(...SEED_TASK_INVALIDATION_TAGS[key]); + } + }); + // Deduplicate + return [...new Set(tags)]; + }, + }), + getSeedStatus: build.query({ + query: (executionId) => ({ + url: `plus/seed/status/${executionId}`, + }), + }), + getLatestSeedStatus: build.query({ + query: () => ({ + url: "plus/seed/status", + }), + }), + }), +}); + +export const { + useTriggerSeedMutation, + useGetSeedStatusQuery, + useGetLatestSeedStatusQuery, +} = seedDataApi; diff --git a/clients/admin-ui/src/pages/poc/seed-data.tsx b/clients/admin-ui/src/pages/poc/seed-data.tsx new file mode 100644 index 0000000000..a2ee4cbc2b --- /dev/null +++ b/clients/admin-ui/src/pages/poc/seed-data.tsx @@ -0,0 +1,22 @@ +import { Layout, Typography } from "fidesui"; +import type { NextPage } from "next"; + +import PageHeader from "~/features/common/PageHeader"; +import SeedDataPanel from "~/features/seed-data/SeedDataPanel"; + +const { Content } = Layout; +const { Paragraph } = Typography; + +const SeedDataPage: NextPage = () => { + return ( + + + + Seed demo data into this environment for testing and development. + + + + ); +}; + +export default SeedDataPage; From 4fc8a7da290fff0b8ed8503f2f2b6fd0a212767c Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Tue, 24 Mar 2026 16:18:11 -0700 Subject: [PATCH 02/10] Add DashboardSnapshot model for trend sparklines - New DashboardSnapshot model with snapshot_date, metric_key, value columns - Unique constraint on (snapshot_date, metric_key) for daily snapshots - Alembic migration to create dashboardsnapshot table - Rename PostureCard title to "Governance posture" Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/admin-ui/src/home/PostureCard.tsx | 2 +- ...700_29113e44faec_add_dashboard_snapshot.py | 67 +++++++++++++++++++ src/fides/api/db/base.py | 1 + src/fides/api/models/dashboard_snapshot.py | 27 ++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py create mode 100644 src/fides/api/models/dashboard_snapshot.py diff --git a/clients/admin-ui/src/home/PostureCard.tsx b/clients/admin-ui/src/home/PostureCard.tsx index 387e8c9381..28594a4db6 100644 --- a/clients/admin-ui/src/home/PostureCard.tsx +++ b/clients/admin-ui/src/home/PostureCard.tsx @@ -88,7 +88,7 @@ export const PostureCard = () => { return ( diff --git a/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py b/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py new file mode 100644 index 0000000000..d6d4df42f7 --- /dev/null +++ b/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py @@ -0,0 +1,67 @@ +"""add dashboard_snapshot table + +Revision ID: 29113e44faec +Revises: 94273d7e8319 +Create Date: 2026-03-24 17:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "29113e44faec" +down_revision = "94273d7e8319" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "dashboardsnapshot", + sa.Column("id", sa.String(length=255), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("snapshot_date", sa.Date(), nullable=False), + sa.Column("metric_key", sa.String(), nullable=False), + sa.Column("value", sa.Float(), nullable=False), + sa.Column( + "metadata", + postgresql.JSONB(astext_type=sa.Text()), + server_default="{}", + nullable=True, + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "snapshot_date", + "metric_key", + name="uq_dashboard_snapshot_date_metric", + ), + ) + op.create_index( + "ix_dashboardsnapshot_snapshot_date", + "dashboardsnapshot", + ["snapshot_date"], + ) + op.create_index( + "ix_dashboardsnapshot_metric_key", + "dashboardsnapshot", + ["metric_key"], + ) + + +def downgrade(): + op.drop_index("ix_dashboardsnapshot_metric_key", table_name="dashboardsnapshot") + op.drop_index("ix_dashboardsnapshot_snapshot_date", table_name="dashboardsnapshot") + op.drop_table("dashboardsnapshot") diff --git a/src/fides/api/db/base.py b/src/fides/api/db/base.py index c204a0062e..c8c5aeaddd 100644 --- a/src/fides/api/db/base.py +++ b/src/fides/api/db/base.py @@ -16,6 +16,7 @@ from fides.api.models.custom_asset import CustomAsset from fides.api.models.custom_connector_template import CustomConnectorTemplate from fides.api.models.custom_report import CustomReport +from fides.api.models.dashboard_snapshot import DashboardSnapshot from fides.api.models.datasetconfig import DatasetConfig from fides.api.models.db_cache import DBCache from fides.api.models.detection_discovery.core import MonitorConfig, StagedResource diff --git a/src/fides/api/models/dashboard_snapshot.py b/src/fides/api/models/dashboard_snapshot.py new file mode 100644 index 0000000000..e7bc566c3a --- /dev/null +++ b/src/fides/api/models/dashboard_snapshot.py @@ -0,0 +1,27 @@ +"""Daily metric snapshots for dashboard trend sparklines.""" + +from sqlalchemy import Column, Date, Float, String, UniqueConstraint +from sqlalchemy.dialects.postgresql import JSONB + +from fides.api.db.base_class import Base + + +class DashboardSnapshot(Base): + """Stores daily metric values for dashboard trend visualization. + + Each row is one metric on one day. The seed task populates historical + data; a future Celery beat task will append daily snapshots. + """ + + snapshot_date = Column(Date, nullable=False, index=True) + metric_key = Column(String, nullable=False, index=True) + value = Column(Float, nullable=False) + metadata_ = Column("metadata", JSONB, nullable=True, server_default="{}") + + __table_args__ = ( + UniqueConstraint( + "snapshot_date", + "metric_key", + name="uq_dashboard_snapshot_date_metric", + ), + ) From 8fc427c69cada6e32493ed7474cecb4ad7363d55 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Tue, 24 Mar 2026 16:21:33 -0700 Subject: [PATCH 03/10] Add Dashboard scenario to Seed Data page - Add dashboard task to SeedTasksConfig interface - Add cache invalidation tags for dashboard seed task - Add "Dashboard (Landing Page)" scenario to seed data UI Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx | 6 ++++++ clients/admin-ui/src/features/seed-data/seed-data.slice.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx index a861bf09f7..66d51c66fa 100644 --- a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx +++ b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx @@ -33,6 +33,12 @@ const SEED_SCENARIOS: SeedScenario[] = [ description: "Seed data purposes, data consumers, datasets, a mock query log integration, and 60 days of access control history.", }, + { + key: "dashboard", + label: "Dashboard (Landing Page)", + description: + "Populate the landing page dashboard with privacy requests, system coverage metrics, audit activity, staged resources, and 30 days of trend history.", + }, ]; const STATUS_COLORS: Record = { diff --git a/clients/admin-ui/src/features/seed-data/seed-data.slice.ts b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts index c9111219f4..96960a62dd 100644 --- a/clients/admin-ui/src/features/seed-data/seed-data.slice.ts +++ b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts @@ -11,6 +11,7 @@ export interface SeedTasksConfig { email_templates?: boolean; discovery_monitors?: boolean; pbac?: boolean; + dashboard?: boolean; } export interface SeedRequest { @@ -60,6 +61,11 @@ const SEED_TASK_INVALIDATION_TAGS: Record = { discovery_monitors: ["Discovery Monitor Configs", "Datastore Connection"], messaging_mailgun: ["Messaging Config"], storage_s3: ["Configuration Settings"], + dashboard: [ + "System", + "Privacy Requests", + "Fides Dashboard", + ], }; const seedDataApi = baseApi.injectEndpoints({ From d1b88edbcfd8bec20f5dd7a29b52507a079f27c5 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Tue, 24 Mar 2026 16:35:06 -0700 Subject: [PATCH 04/10] Rename table to dashboard_snapshot and fix migration chain - Set explicit __tablename__ = "dashboard_snapshot" on model - Update migration table/index names to use dashboard_snapshot - Fix down_revision to chain after 190e4603ad38 (resolves multiple heads) Co-Authored-By: Claude Opus 4.6 (1M context) --- ...1700_29113e44faec_add_dashboard_snapshot.py | 18 +++++++++--------- src/fides/api/models/dashboard_snapshot.py | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py b/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py index d6d4df42f7..5a50d61e0d 100644 --- a/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py +++ b/src/fides/api/alembic/migrations/versions/xx_2026_03_24_1700_29113e44faec_add_dashboard_snapshot.py @@ -12,14 +12,14 @@ # revision identifiers, used by Alembic. revision = "29113e44faec" -down_revision = "94273d7e8319" +down_revision = "190e4603ad38" branch_labels = None depends_on = None def upgrade(): op.create_table( - "dashboardsnapshot", + "dashboard_snapshot", sa.Column("id", sa.String(length=255), nullable=False), sa.Column( "created_at", @@ -50,18 +50,18 @@ def upgrade(): ), ) op.create_index( - "ix_dashboardsnapshot_snapshot_date", - "dashboardsnapshot", + "ix_dashboard_snapshot_snapshot_date", + "dashboard_snapshot", ["snapshot_date"], ) op.create_index( - "ix_dashboardsnapshot_metric_key", - "dashboardsnapshot", + "ix_dashboard_snapshot_metric_key", + "dashboard_snapshot", ["metric_key"], ) def downgrade(): - op.drop_index("ix_dashboardsnapshot_metric_key", table_name="dashboardsnapshot") - op.drop_index("ix_dashboardsnapshot_snapshot_date", table_name="dashboardsnapshot") - op.drop_table("dashboardsnapshot") + op.drop_index("ix_dashboard_snapshot_metric_key", table_name="dashboard_snapshot") + op.drop_index("ix_dashboard_snapshot_snapshot_date", table_name="dashboard_snapshot") + op.drop_table("dashboard_snapshot") diff --git a/src/fides/api/models/dashboard_snapshot.py b/src/fides/api/models/dashboard_snapshot.py index e7bc566c3a..59cd95b460 100644 --- a/src/fides/api/models/dashboard_snapshot.py +++ b/src/fides/api/models/dashboard_snapshot.py @@ -13,6 +13,8 @@ class DashboardSnapshot(Base): data; a future Celery beat task will append daily snapshots. """ + __tablename__ = "dashboard_snapshot" + snapshot_date = Column(Date, nullable=False, index=True) metric_key = Column(String, nullable=False, index=True) value = Column(Float, nullable=False) From 4fbea882e3276f8e27deb48c4eca1e1aba755a23 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Tue, 24 Mar 2026 16:38:57 -0700 Subject: [PATCH 05/10] Rename GPS Score to Governance Posture in TrendCard Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/admin-ui/src/home/TrendCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/home/TrendCard.tsx b/clients/admin-ui/src/home/TrendCard.tsx index 4b95755fff..f8a39af0ad 100644 --- a/clients/admin-ui/src/home/TrendCard.tsx +++ b/clients/admin-ui/src/home/TrendCard.tsx @@ -9,7 +9,7 @@ import cardStyles from "./dashboard-card.module.scss"; type StatType = "number" | "percent"; const TREND_METRIC_CONFIG = { - gps_score: { label: "GPS Score", statType: "number" }, + gps_score: { label: "Governance Posture", statType: "number" }, dsr_volume: { label: "DSR Volume", statType: "number" }, system_coverage: { label: "System Coverage", statType: "percent" }, classification_health: { From 9d786444dd15ac07109f3d5a563fc361d82454a0 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Tue, 24 Mar 2026 18:04:47 -0700 Subject: [PATCH 06/10] Add is_overdue filter, dashboard UI polish, and tests Backend: - Add ACTIVE_REQUEST_STATUSES central definition to privacy_request schemas - Add is_overdue filter to PrivacyRequestFilter and query utils (filters to active statuses with due_date in the past) Frontend: - DSR status and briefing links filter by action_type and is_overdue via URL params (nuqs) instead of Redux dispatch - Sentence case card titles (DSR status, System coverage, SLA health) - Dynamic YAxis width in StackedBarChart based on longest label - Sort SLA bars alphabetically (Access before Erasure) - "X days overdue" badge for negative days instead of "-X days left" - Equal height System coverage and DSR status cards - ACTION_CTA supports is_overdue routing for DSR actions Tests: - ACTIVE_REQUEST_STATUSES membership and exclusion tests - PrivacyRequestFilter is_overdue field acceptance tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/dashboard/constants.ts | 7 +++- .../hooks/usePrivacyRequestsFilters.ts | 5 +++ .../list-item/components/DaysLeft.tsx | 10 ++++- .../privacy-requests.slice.ts | 8 ++++ .../src/home/DSRStatusCard.module.scss | 9 ++++ clients/admin-ui/src/home/DSRStatusCard.tsx | 23 ++++++---- clients/admin-ui/src/home/HomeDashboard.tsx | 12 +++--- .../admin-ui/src/home/SystemCoverageCard.tsx | 4 +- .../src/components/charts/StackedBarChart.tsx | 29 ++++++++++--- src/fides/api/schemas/privacy_request.py | 14 +++++++ .../privacy_request_query_utils.py | 16 +++++++ tests/unit/test_active_request_statuses.py | 42 +++++++++++++++++++ 12 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 tests/unit/test_active_request_statuses.py diff --git a/clients/admin-ui/src/features/dashboard/constants.ts b/clients/admin-ui/src/features/dashboard/constants.ts index 12f8f91b3e..1bbcf56d92 100644 --- a/clients/admin-ui/src/features/dashboard/constants.ts +++ b/clients/admin-ui/src/features/dashboard/constants.ts @@ -74,8 +74,11 @@ export const ACTION_CTA: Record< }, [ActionType.DSR_ACTION]: { label: "View request", - route: (d) => - d.request_id ? `/privacy-requests/${d.request_id}` : "/privacy-requests", + route: (d) => { + if (d.request_id) return `/privacy-requests/${d.request_id}`; + if (d.is_overdue) return "/privacy-requests?is_overdue=true"; + return "/privacy-requests"; + }, }, [ActionType.SYSTEM_REVIEW]: { label: "Review system", diff --git a/clients/admin-ui/src/features/privacy-requests/dashboard/hooks/usePrivacyRequestsFilters.ts b/clients/admin-ui/src/features/privacy-requests/dashboard/hooks/usePrivacyRequestsFilters.ts index e78c71e20e..f0311372a1 100644 --- a/clients/admin-ui/src/features/privacy-requests/dashboard/hooks/usePrivacyRequestsFilters.ts +++ b/clients/admin-ui/src/features/privacy-requests/dashboard/hooks/usePrivacyRequestsFilters.ts @@ -1,5 +1,6 @@ import { parseAsArrayOf, + parseAsBoolean, parseAsString, parseAsStringEnum, useQueryStates, @@ -18,6 +19,7 @@ export interface FilterQueryParams { to: string | null; status: PrivacyRequestStatus[] | null; action_type: ActionType[] | null; + is_overdue: boolean | null; location: string | null; custom_privacy_request_fields: Record | null; sort_field: string | null; @@ -46,6 +48,7 @@ const usePrivacyRequestsFilters = ({ to: parseAsString, status: parseAsArrayOf(parseAsStringEnum(allowedStatusFilterOptions)), action_type: parseAsArrayOf(parseAsStringEnum(Object.values(ActionType))), + is_overdue: parseAsBoolean, location: parseAsString, custom_privacy_request_fields: parseAsCustomFields, }, @@ -71,6 +74,7 @@ const usePrivacyRequestsFilters = ({ to: filters.to, status: filters.status, action_type: filters.action_type, + is_overdue: filters.is_overdue, location: filters.location, custom_privacy_request_fields: filterNullCustomFields( filters.custom_privacy_request_fields, @@ -84,6 +88,7 @@ const usePrivacyRequestsFilters = ({ filters.to, filters.status, filters.action_type, + filters.is_overdue, filters.location, filters.custom_privacy_request_fields, sortState.sort_field, diff --git a/clients/admin-ui/src/features/privacy-requests/dashboard/list-item/components/DaysLeft.tsx b/clients/admin-ui/src/features/privacy-requests/dashboard/list-item/components/DaysLeft.tsx index 05f503114b..94ba0f4965 100644 --- a/clients/admin-ui/src/features/privacy-requests/dashboard/list-item/components/DaysLeft.tsx +++ b/clients/admin-ui/src/features/privacy-requests/dashboard/list-item/components/DaysLeft.tsx @@ -29,14 +29,20 @@ export const DaysLeft = ({ !DAY_IRRELEVANT_STATUSES.includes(status); if (showBadge) { + const isOverdue = daysLeft < 0; const percentage = (100 * daysLeft) / timeframe; const color = - percentage < 25 ? CUSTOM_TAG_COLOR.ERROR : CUSTOM_TAG_COLOR.DEFAULT; + isOverdue || percentage < 25 + ? CUSTOM_TAG_COLOR.ERROR + : CUSTOM_TAG_COLOR.DEFAULT; + const label = isOverdue + ? `${Math.abs(daysLeft)} days overdue` + : `${daysLeft} days left`; return (
- <>{daysLeft} days left + <>{label}
diff --git a/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts b/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts index 11d47376a1..cb3bdb1ce7 100644 --- a/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts +++ b/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts @@ -146,6 +146,7 @@ export const selectPrivacyRequestFilters = createSelector( from: subjectRequests.from, id: subjectRequests.id, fuzzy_search_str: subjectRequests.fuzzy_search_str, + is_overdue: subjectRequests.is_overdue, page: subjectRequests.page, size: subjectRequests.size, sort_direction: subjectRequests.sort_direction, @@ -175,6 +176,7 @@ type SubjectRequestsState = { from: string; id: string; fuzzy_search_str?: string; + is_overdue?: boolean; page: number; size: number; sort_direction?: string; @@ -251,6 +253,11 @@ export const subjectRequestsSlice = createSlice({ page: initialState.page, action_type: action.payload, }), + setRequestOverdue: (state, action: PayloadAction) => ({ + ...state, + page: initialState.page, + is_overdue: action.payload, + }), setRequestTo: (state, action: PayloadAction) => ({ ...state, page: initialState.page, @@ -289,6 +296,7 @@ export const { setRequestId, setRequestStatus, setRequestActionType, + setRequestOverdue, setRequestTo, setRetryRequests, setSortDirection, diff --git a/clients/admin-ui/src/home/DSRStatusCard.module.scss b/clients/admin-ui/src/home/DSRStatusCard.module.scss index cf0cec3245..7ff51ed242 100644 --- a/clients/admin-ui/src/home/DSRStatusCard.module.scss +++ b/clients/admin-ui/src/home/DSRStatusCard.module.scss @@ -1,9 +1,18 @@ +.spinWrapper { + height: 100%; + + :global(.ant-spin-container) { + height: 100%; + } +} + .cardContainer { height: 100%; :global(.ant-card-body) { display: flex; flex-direction: column; + flex: 1; } :global(.ant-statistic) { diff --git a/clients/admin-ui/src/home/DSRStatusCard.tsx b/clients/admin-ui/src/home/DSRStatusCard.tsx index 85bda6f49e..7e64dc7e4e 100644 --- a/clients/admin-ui/src/home/DSRStatusCard.tsx +++ b/clients/admin-ui/src/home/DSRStatusCard.tsx @@ -38,15 +38,21 @@ export const DSRStatusCard = () => { const handleTypeClick = useCallback( (type: string) => { - router.push(`${PRIVACY_REQUESTS_ROUTE}?action_type=${type}`); + router.push( + `${PRIVACY_REQUESTS_ROUTE}?action_type=${type.toLowerCase()}`, + ); }, [router], ); + const handleOverdueClick = useCallback(() => { + router.push(`${PRIVACY_REQUESTS_ROUTE}?is_overdue=true`); + }, [router]); + return ( - + { {SUB_STATS.map(({ key, title }) => ( { {(data?.overdue_count ?? 0) > 0 && ( - @@ -101,13 +108,13 @@ export const DSRStatusCard = () => { - + )} - SLA Health + SLA health {sla && ( diff --git a/clients/admin-ui/src/home/HomeDashboard.tsx b/clients/admin-ui/src/home/HomeDashboard.tsx index ec14a6a43d..dfa2c4da5a 100644 --- a/clients/admin-ui/src/home/HomeDashboard.tsx +++ b/clients/admin-ui/src/home/HomeDashboard.tsx @@ -70,14 +70,14 @@ export const HomeDashboard = () => { ))} - - +
+
- - +
+
- - +
+
); diff --git a/clients/admin-ui/src/home/SystemCoverageCard.tsx b/clients/admin-ui/src/home/SystemCoverageCard.tsx index f8da4f3c33..ca79896adc 100644 --- a/clients/admin-ui/src/home/SystemCoverageCard.tsx +++ b/clients/admin-ui/src/home/SystemCoverageCard.tsx @@ -18,7 +18,7 @@ export const SystemCoverageCard = () => { return ( - +
@@ -59,7 +59,7 @@ export const SystemCoverageCard = () => { diff --git a/clients/fidesui/src/components/charts/StackedBarChart.tsx b/clients/fidesui/src/components/charts/StackedBarChart.tsx index cfce26984e..f6037cbdf5 100644 --- a/clients/fidesui/src/components/charts/StackedBarChart.tsx +++ b/clients/fidesui/src/components/charts/StackedBarChart.tsx @@ -203,17 +203,36 @@ export const StackedBarChart = ({ .filter((entry): entry is ChartEntry => entry !== null); }, [data, segments]); - if (chartData.length === 0) { + // Sort alphabetically so ordering is deterministic (e.g. Access before Erasure) + const sortedData = useMemo( + () => [...chartData].sort((a, b) => a.category.localeCompare(b.category)), + [chartData], + ); + + // Compute YAxis width dynamically from the longest label + const yAxisWidth = useMemo(() => { + if (sortedData.length === 0) return 80; + const longestLabel = sortedData.reduce( + (max, entry) => + entry.category.length > max.length ? entry.category : max, + "", + ); + const charWidth = token.fontSizeSM * 0.65; + const iconSpace = onCategoryClick ? ICON_SIZE + ICON_GAP + 8 : 8; + return Math.ceil(longestLabel.length * charWidth + iconSpace); + }, [sortedData, token.fontSizeSM, onCategoryClick]); + + if (sortedData.length === 0) { return null; } const chartHeight = - chartData.length * barHeight + (chartData.length - 1) * rowGap + rowGap; + sortedData.length * barHeight + (sortedData.length - 1) * rowGap + rowGap; return ( } tickLine={false} diff --git a/src/fides/api/schemas/privacy_request.py b/src/fides/api/schemas/privacy_request.py index 8ab39eb3b4..37aa6d0e7d 100644 --- a/src/fides/api/schemas/privacy_request.py +++ b/src/fides/api/schemas/privacy_request.py @@ -323,6 +323,19 @@ class PrivacyRequestStatus(StrEnum): duplicate = "duplicate" # Request identified as duplicate of another request +ACTIVE_REQUEST_STATUSES = frozenset( + { + PrivacyRequestStatus.in_processing, + PrivacyRequestStatus.pending, + PrivacyRequestStatus.approved, + PrivacyRequestStatus.paused, + PrivacyRequestStatus.requires_input, + PrivacyRequestStatus.requires_manual_finalization, + PrivacyRequestStatus.pending_external, + } +) + + class IdentityValue(BaseModel): """Represents an identity value with a label in API responses. @@ -573,6 +586,7 @@ class PrivacyRequestFilter(FidesSchema): include_identities: Optional[bool] = False include_custom_privacy_request_fields: Optional[bool] = False include_deleted_requests: Optional[bool] = False + is_overdue: Optional[bool] = None download_csv: Optional[bool] = False sort_field: str = "created_at" sort_direction: ColumnSort = ColumnSort.DESC diff --git a/src/fides/service/privacy_request/privacy_request_query_utils.py b/src/fides/service/privacy_request/privacy_request_query_utils.py index 7689daf564..ab3f2af002 100644 --- a/src/fides/service/privacy_request/privacy_request_query_utils.py +++ b/src/fides/service/privacy_request/privacy_request_query_utils.py @@ -1,4 +1,5 @@ # pylint: disable=too-many-branches, too-many-statements +from datetime import datetime, timezone from typing import Optional from sqlalchemy.orm import Session @@ -14,6 +15,7 @@ ) from fides.api.schemas.policy import ActionType from fides.api.schemas.privacy_request import ( + ACTIVE_REQUEST_STATUSES, MAX_BULK_FILTER_RESULTS, PrivacyRequestBulkSelection, PrivacyRequestFilter, @@ -223,6 +225,20 @@ def filter_privacy_request_queryset( ) query = query.filter(PrivacyRequest.policy_id.in_(policy_ids_for_action_type)) + if filters.is_overdue is True: + query = query.filter( + PrivacyRequest.due_date.isnot(None), + PrivacyRequest.due_date < datetime.now(timezone.utc), + PrivacyRequest.status.in_(list(ACTIVE_REQUEST_STATUSES)), + ) + elif filters.is_overdue is False: + query = query.filter( + or_( + PrivacyRequest.due_date.is_(None), + PrivacyRequest.due_date >= datetime.now(timezone.utc), + ) + ) + if not include_consent_webhook_requests: query = query.filter( or_( diff --git a/tests/unit/test_active_request_statuses.py b/tests/unit/test_active_request_statuses.py new file mode 100644 index 0000000000..d0a7652121 --- /dev/null +++ b/tests/unit/test_active_request_statuses.py @@ -0,0 +1,42 @@ +"""Tests for ACTIVE_REQUEST_STATUSES and is_overdue filter.""" + +from fides.api.schemas.privacy_request import ( + ACTIVE_REQUEST_STATUSES, + PrivacyRequestFilter, + PrivacyRequestStatus, +) + + +class TestActiveRequestStatuses: + def test_contains_expected_active_statuses(self): + assert PrivacyRequestStatus.in_processing in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.pending in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.approved in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.paused in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.requires_input in ACTIVE_REQUEST_STATUSES + assert ( + PrivacyRequestStatus.requires_manual_finalization in ACTIVE_REQUEST_STATUSES + ) + assert PrivacyRequestStatus.pending_external in ACTIVE_REQUEST_STATUSES + + def test_excludes_terminal_statuses(self): + assert PrivacyRequestStatus.complete not in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.canceled not in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.error not in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.denied not in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.duplicate not in ACTIVE_REQUEST_STATUSES + assert PrivacyRequestStatus.identity_unverified not in ACTIVE_REQUEST_STATUSES + + +class TestPrivacyRequestFilterOverdue: + def test_accepts_true(self): + f = PrivacyRequestFilter(is_overdue=True) + assert f.is_overdue is True + + def test_accepts_false(self): + f = PrivacyRequestFilter(is_overdue=False) + assert f.is_overdue is False + + def test_defaults_to_none(self): + f = PrivacyRequestFilter() + assert f.is_overdue is None From 230d07f744be259aa8514646e8be6ef35ef00d98 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Wed, 25 Mar 2026 08:53:49 -0700 Subject: [PATCH 07/10] Misc fixes --- .../admin-ui/src/home/PriorityActionsCard.tsx | 24 ++++++++++++++++++- clients/admin-ui/src/home/TrendCard.tsx | 8 +++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/clients/admin-ui/src/home/PriorityActionsCard.tsx b/clients/admin-ui/src/home/PriorityActionsCard.tsx index 2e3b1ef300..ce17e675af 100644 --- a/clients/admin-ui/src/home/PriorityActionsCard.tsx +++ b/clients/admin-ui/src/home/PriorityActionsCard.tsx @@ -12,6 +12,9 @@ import { import NextLink from "next/link"; import { type ReactNode, useMemo, useState } from "react"; +import RequestStatusBadge, { + statusPropMap, +} from "~/features/common/RequestStatusBadge"; import { ACTION_CTA, DIMENSION_LABELS, @@ -134,7 +137,26 @@ export const PriorityActionsCard = () => { /> } title={action.title} - description={action.message} + description={ + action.action_data?.status && + action.action_data.status in statusPropMap ? ( + + + + + {action.message} + + + + ) : ( + action.message + ) + } /> ); diff --git a/clients/admin-ui/src/home/TrendCard.tsx b/clients/admin-ui/src/home/TrendCard.tsx index f8a39af0ad..13511500ea 100644 --- a/clients/admin-ui/src/home/TrendCard.tsx +++ b/clients/admin-ui/src/home/TrendCard.tsx @@ -9,11 +9,11 @@ import cardStyles from "./dashboard-card.module.scss"; type StatType = "number" | "percent"; const TREND_METRIC_CONFIG = { - gps_score: { label: "Governance Posture", statType: "number" }, - dsr_volume: { label: "DSR Volume", statType: "number" }, - system_coverage: { label: "System Coverage", statType: "percent" }, + gps_score: { label: "Governance posture", statType: "number" }, + dsr_volume: { label: "DSR volume", statType: "number" }, + system_coverage: { label: "System coverage", statType: "percent" }, classification_health: { - label: "Classification Health", + label: "Classification health", statType: "percent", }, } satisfies Record; From b47101936c6b48c88314101e0943a31dd41fdcd9 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Wed, 25 Mar 2026 09:23:37 -0700 Subject: [PATCH 08/10] Address PR review feedback on SeedDataPanel - Surface trigger errors via RTK Query isError state and inline Alert - Add clarifying comment for 2s polling interval (dev-only tool) - Rename single-char variable `s` to `step` in computeProgress - Replace bare div with Flex component for semantic layout Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/seed-data/SeedDataPanel.tsx | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx index 66d51c66fa..7dffb68cab 100644 --- a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx +++ b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx @@ -65,8 +65,10 @@ const computeProgress = (steps: Record): number => { return 0; } const done = entries.filter( - (s) => - s.status === "complete" || s.status === "skipped" || s.status === "error", + (step) => + step.status === "complete" || + step.status === "skipped" || + step.status === "error", ).length; return Math.round((done / entries.length) * 100); }; @@ -76,11 +78,13 @@ const SeedDataPanel = () => { Set >(new Set()); const [executionId, setExecutionId] = useState(null); - const [triggerSeed, { isLoading: isTriggering }] = useTriggerSeedMutation(); + const [triggerSeed, { isLoading: isTriggering, isError: isTriggerError }] = + useTriggerSeedMutation(); // Poll for status when an execution is in progress const { data: statusData } = useGetSeedStatusQuery(executionId!, { skip: !executionId, + // Seed tasks complete within seconds; 2s polling is intentional for this dev-only tool. pollingInterval: 2000, }); @@ -112,11 +116,11 @@ const SeedDataPanel = () => { tasks[key] = true; }); - try { - const result = await triggerSeed({ tasks }).unwrap(); + const result = await triggerSeed({ tasks }) + .unwrap() + .catch(() => null); + if (result) { setExecutionId(result.execution_id); - } catch { - // Error is shown via the status polling } }, [selectedTasks, triggerSeed]); @@ -156,8 +160,17 @@ const SeedDataPanel = () => { + {isTriggerError ? ( + + ) : null} + {showStatus ? ( -
+ { showIcon /> ) : null} -
+ ) : null} ); From 05f3ac7a74996942fe2d2bd55c339a6b51798105 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Wed, 25 Mar 2026 09:31:24 -0700 Subject: [PATCH 09/10] Fix mypy and TypeScript type errors - Add type: ignore for __tablename__ override in DashboardSnapshot - Use `as const` for STATUS_COLORS to satisfy Tag color prop types - Cast invalidatesTags return to satisfy RTK Query TagDescription type - Add is_overdue to PrivacyRequestParams interface - Add typeof guard before `in` operator for action_data.status Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/admin-ui/src/features/privacy-requests/types.ts | 1 + clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx | 7 ++++--- clients/admin-ui/src/features/seed-data/seed-data.slice.ts | 4 ++-- clients/admin-ui/src/home/PriorityActionsCard.tsx | 1 + src/fides/api/models/dashboard_snapshot.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 210cfb305f..bf360e2774 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -130,6 +130,7 @@ export interface PrivacyRequestParams { id: string; from: string; to: string; + is_overdue?: boolean; page: number; size: number; verbose?: boolean; diff --git a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx index 7dffb68cab..531e2c57e6 100644 --- a/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx +++ b/clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx @@ -41,16 +41,17 @@ const SEED_SCENARIOS: SeedScenario[] = [ }, ]; -const STATUS_COLORS: Record = { +const STATUS_COLORS = { pending: "default", in_progress: "processing", complete: "success", skipped: "warning", error: "error", -}; +} as const; const StepStatusTag = ({ status }: { status: SeedStepStatus }) => { - const color = STATUS_COLORS[status.status] ?? "default"; + const color = + STATUS_COLORS[status.status as keyof typeof STATUS_COLORS] ?? "default"; const style = color === "processing" ? { color: "white" } : undefined; return ( diff --git a/clients/admin-ui/src/features/seed-data/seed-data.slice.ts b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts index 96960a62dd..76a5c9206e 100644 --- a/clients/admin-ui/src/features/seed-data/seed-data.slice.ts +++ b/clients/admin-ui/src/features/seed-data/seed-data.slice.ts @@ -86,8 +86,8 @@ const seedDataApi = baseApi.injectEndpoints({ tags.push(...SEED_TASK_INVALIDATION_TAGS[key]); } }); - // Deduplicate - return [...new Set(tags)]; + // Deduplicate and cast to satisfy RTK Query's TagDescription type + return [...new Set(tags)] as typeof tags; }, }), getSeedStatus: build.query({ diff --git a/clients/admin-ui/src/home/PriorityActionsCard.tsx b/clients/admin-ui/src/home/PriorityActionsCard.tsx index ce17e675af..4c85bd3507 100644 --- a/clients/admin-ui/src/home/PriorityActionsCard.tsx +++ b/clients/admin-ui/src/home/PriorityActionsCard.tsx @@ -139,6 +139,7 @@ export const PriorityActionsCard = () => { title={action.title} description={ action.action_data?.status && + typeof action.action_data.status === "string" && action.action_data.status in statusPropMap ? ( diff --git a/src/fides/api/models/dashboard_snapshot.py b/src/fides/api/models/dashboard_snapshot.py index 59cd95b460..64a79f0717 100644 --- a/src/fides/api/models/dashboard_snapshot.py +++ b/src/fides/api/models/dashboard_snapshot.py @@ -13,7 +13,7 @@ class DashboardSnapshot(Base): data; a future Celery beat task will append daily snapshots. """ - __tablename__ = "dashboard_snapshot" + __tablename__ = "dashboard_snapshot" # type: ignore[assignment] snapshot_date = Column(Date, nullable=False, index=True) metric_key = Column(String, nullable=False, index=True) From fdd76546383eea58ea2330535054d50992352e11 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Wed, 25 Mar 2026 09:33:53 -0700 Subject: [PATCH 10/10] Register tests/unit/ in CI test coverage mapping Add tests/unit/ to TEST_DIRECTORY_COVERAGE and pytest_misc_unit so the collect_tests check recognizes the new directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- noxfiles/ci_nox.py | 1 + noxfiles/setup_tests_nox.py | 1 + 2 files changed, 2 insertions(+) diff --git a/noxfiles/ci_nox.py b/noxfiles/ci_nox.py index 513ac67bb8..dea8f8f7d3 100644 --- a/noxfiles/ci_nox.py +++ b/noxfiles/ci_nox.py @@ -560,6 +560,7 @@ def pytest_redis_cluster_docker(session: nox.Session) -> None: "misc-integration-external", ], "tests/task/": ["misc-unit", "misc-integration", "misc-integration-external"], + "tests/unit/": ["misc-unit"], "tests/util/": ["misc-unit", "misc-integration", "misc-integration-external"], "tests/qa/": ["misc-unit", "misc-integration", "misc-integration-external"], "tests/integration/": ["ops-integration"], # Workflow integration tests diff --git a/noxfiles/setup_tests_nox.py b/noxfiles/setup_tests_nox.py index a9c0b46e21..2002d9665c 100644 --- a/noxfiles/setup_tests_nox.py +++ b/noxfiles/setup_tests_nox.py @@ -462,6 +462,7 @@ def pytest_misc_unit(session: Session, pytest_config: PytestConfig) -> None: "tests/service/", "tests/system_integration_link/", "tests/task/", + "tests/unit/", "tests/util/", "-m", "not integration and not integration_external and not integration_saas and not integration_snowflake and not integration_bigquery and not integration_postgres",