Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions clients/admin-ui/src/features/common/nav/nav-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,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,
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/common/nav/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

// RBAC routes
Expand Down
7 changes: 5 additions & 2 deletions clients/admin-ui/src/features/dashboard/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
parseAsArrayOf,
parseAsBoolean,
parseAsString,
parseAsStringEnum,
useQueryStates,
Expand All @@ -19,6 +20,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<string, string | number> | null;
sort_field: string | null;
Expand Down Expand Up @@ -52,6 +54,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,
},
Expand All @@ -77,6 +80,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,
Expand All @@ -90,6 +94,7 @@ const usePrivacyRequestsFilters = ({
filters.to,
filters.status,
filters.action_type,
filters.is_overdue,
filters.location,
filters.custom_privacy_request_fields,
sortState.sort_field,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<Tag color={color} variant="filled">
<Tooltip title={formatDate(dayjs().add(daysLeft, "day").toDate())}>
<>{daysLeft} days left</>
<>{label}</>
</Tooltip>
</Tag>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -175,6 +176,7 @@ type SubjectRequestsState = {
from: string;
id: string;
fuzzy_search_str?: string;
is_overdue?: boolean;
page: number;
size: number;
sort_direction?: string;
Expand Down Expand Up @@ -251,6 +253,11 @@ export const subjectRequestsSlice = createSlice({
page: initialState.page,
action_type: action.payload,
}),
setRequestOverdue: (state, action: PayloadAction<boolean | undefined>) => ({
...state,
page: initialState.page,
is_overdue: action.payload,
}),
setRequestTo: (state, action: PayloadAction<string>) => ({
...state,
page: initialState.page,
Expand Down Expand Up @@ -289,6 +296,7 @@ export const {
setRequestId,
setRequestStatus,
setRequestActionType,
setRequestOverdue,
setRequestTo,
setRetryRequests,
setSortDirection,
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/privacy-requests/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface PrivacyRequestParams {
id: string;
from: string;
to: string;
is_overdue?: boolean;
page: number;
size: number;
verbose?: boolean;
Expand Down
209 changes: 209 additions & 0 deletions clients/admin-ui/src/features/seed-data/SeedDataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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.",
},
{
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 = {
pending: "default",
in_progress: "processing",
complete: "success",
skipped: "warning",
error: "error",
} as const;

const StepStatusTag = ({ status }: { status: SeedStepStatus }) => {
const color =
STATUS_COLORS[status.status as keyof typeof STATUS_COLORS] ?? "default";
const style = color === "processing" ? { color: "white" } : undefined;
return (
<Tag color={color} style={style}>
{status.status.replace("_", " ")}
</Tag>
);
};

const computeProgress = (steps: Record<string, SeedStepStatus>): number => {
const entries = Object.values(steps);
if (entries.length === 0) {
return 0;
}
const done = entries.filter(
(step) =>
step.status === "complete" ||
step.status === "skipped" ||
step.status === "error",
).length;
Comment on lines +68 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Single-character variable name

The parameter s should use a full descriptive name per the project's variable naming convention.

Suggested change
const done = entries.filter(
(s) =>
s.status === "complete" || s.status === "skipped" || s.status === "error",
).length;
const done = entries.filter(
(step) =>
step.status === "complete" || step.status === "skipped" || step.status === "error",
).length;

Rule Used: Use full names for variables, not 1 to 2 character... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b471019. Renamed s to step.

return Math.round((done / entries.length) * 100);
};

const SeedDataPanel = () => {
const [selectedTasks, setSelectedTasks] = useState<
Set<keyof SeedTasksConfig>
>(new Set());
const [executionId, setExecutionId] = useState<string | null>(null);
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,
});
Comment on lines +86 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Development polling interval should be updated before merging

pollingInterval: 2000 (2 s) is a development-only value. Per the project's polling interval guidance, development values should be reviewed and updated before merging — even for dev-mode-only pages — to ensure intentional configuration. If a 2-second interval is the deliberate choice for this lightweight tool, add an explanatory comment so reviewers know it was considered.

Suggested change
const { data: statusData } = useGetSeedStatusQuery(executionId!, {
skip: !executionId,
pollingInterval: 2000,
});
const { data: statusData } = useGetSeedStatusQuery(executionId!, {
skip: !executionId,
// Seed tasks complete within seconds; 2 s polling is intentional for this dev-only tool.
pollingInterval: 2000,
});

Rule Used: Polling intervals for async operations should be s... (source)

Learnt From
ethyca/fides#6566

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b471019. Added a clarifying comment that the 2s interval is intentional for this dev-only tool.


// 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;
});

const result = await triggerSeed({ tasks })
.unwrap()
.catch(() => null);
if (result) {
setExecutionId(result.execution_id);
}
}, [selectedTasks, triggerSeed]);

const isRunning = !!executionId || isTriggering;
const showStatus = statusData && statusData.status !== "pending";

return (
<Card title="Seed scenarios" style={{ maxWidth: 720 }}>
<Paragraph type="secondary">
Select which data scenarios to seed into this environment. Seeding is
idempotent — it is safe to run multiple times.
</Paragraph>

<Space direction="vertical" size="middle" className="mb-6 w-full">
{SEED_SCENARIOS.map((scenario) => (
<Checkbox
key={scenario.key}
checked={selectedTasks.has(scenario.key)}
onChange={() => handleToggle(scenario.key)}
disabled={isRunning}
>
<Text strong>{scenario.label}</Text>
<br />
<Text type="secondary">{scenario.description}</Text>
</Checkbox>
))}
</Space>

<Flex gap="small" align="center">
<Button
type="primary"
onClick={handleSeed}
disabled={selectedTasks.size === 0 || isRunning}
loading={isRunning}
>
{isRunning ? "Seeding..." : "Seed data"}
</Button>
</Flex>

{isTriggerError ? (
<Alert
type="error"
message="Failed to start seed. Please try again."
className="mt-4"
showIcon
/>
) : null}

{showStatus ? (
<Flex vertical gap="small" className="mt-4">
<Progress
percent={computeProgress(statusData.steps)}
status={statusData.status === "error" ? "exception" : undefined}
/>
<Space direction="vertical" size="small" className="mt-2 w-full">
{Object.entries(statusData.steps).map(([stepName, stepStatus]) => (
<Flex key={stepName} justify="space-between" align="center">
<Text>{stepName.replace(/_/g, " ")}</Text>
<StepStatusTag status={stepStatus} />
</Flex>
))}
</Space>
{statusData.error ? (
<Alert
type="error"
message={statusData.error}
className="mt-2"
showIcon
/>
) : null}
{statusData.status === "complete" ? (
<Alert
type="success"
message="Seed completed successfully"
className="mt-2"
showIcon
/>
) : null}
</Flex>
) : null}
</Card>
);
};

export default SeedDataPanel;
Loading
Loading