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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useMemo } from "react";

export const useDiscoveredInfrastructureSystemsColumns = () => {
const columns = useMemo(
() => [
{
key: "name",
dataIndex: "name",
title: "System",
sorter: true,
},
],
[],
);

return { columns };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { AntEmpty as Empty } from "fidesui";
import { useCallback, useMemo } from "react";

import { UNCATEGORIZED_SEGMENT } from "~/features/common/nav/routes";
import { useAntTable, useTableState } from "~/features/common/table/hooks";
import { SystemStagedResourcesAggregateRecord } from "~/types/api";

import { useGetDiscoveredSystemAggregateQuery } from "../action-center.slice";
import useActionCenterTabs, {
ActionCenterTabHash,
} from "./useActionCenterTabs";
import { useDiscoveredInfrastructureSystemsColumns } from "./useDiscoveredInfrastructureSystemsColumns";

interface UseDiscoveredInfrastructureSystemsTableConfig {
monitorId: string;
}

export const useDiscoveredInfrastructureSystemsTable = ({
monitorId,
}: UseDiscoveredInfrastructureSystemsTableConfig) => {
const { filterTabs, activeTab, onTabChange, activeParams } =
useActionCenterTabs();

const tableState = useTableState<"name">({
sorting: {
validColumns: ["name"],
},
});

const { pageIndex, pageSize, searchQuery, updateSearch, resetState } =
tableState;

const { data, isLoading, isFetching } = useGetDiscoveredSystemAggregateQuery({
key: monitorId,
page: pageIndex,
size: pageSize,
search: searchQuery,
...activeParams,
});

// Helper function to generate consistent row keys
const getRecordKey = useCallback(
(record: SystemStagedResourcesAggregateRecord) =>
record.id ?? record.vendor_id ?? record.name ?? UNCATEGORIZED_SEGMENT,
[],
);

const antTableConfig = useMemo(
() => ({
enableSelection: true,
getRowKey: getRecordKey,
isLoading,
isFetching,
dataSource: data?.items || [],
totalRows: data?.total || 0,
customTableProps: {
locale: {
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="All caught up!"
/>
),
},
},
}),
[getRecordKey, isLoading, isFetching, data?.items, data?.total],
);

const antTable = useAntTable<SystemStagedResourcesAggregateRecord, "name">(
tableState,
antTableConfig,
);

const { selectedRows, resetSelections } = antTable;

const handleTabChange = useCallback(
async (tab: ActionCenterTabHash) => {
await onTabChange(tab);
resetState();
resetSelections();
},
[onTabChange, resetState, resetSelections],
);

const { columns } = useDiscoveredInfrastructureSystemsColumns();

return {
// Table state and data
columns,
data,
isLoading,
isFetching,
searchQuery,
updateSearch,
resetState,

// Ant Design table integration
tableProps: antTable.tableProps,
selectionProps: antTable.selectionProps,

// Tab management
filterTabs,
activeTab,
handleTabChange,
activeParams,

// Selection
selectedRows,
hasSelectedRows: antTable.hasSelectedRows,
resetSelections,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
AntFlex as Flex,
AntMenu as Menu,
AntSpace as Space,
AntTable as Table,
} from "fidesui";

import { SelectedText } from "~/features/common/table/SelectedText";

import { DebouncedSearchInput } from "../../../common/DebouncedSearchInput";
import { ActionCenterTabHash } from "../hooks/useActionCenterTabs";
import { useDiscoveredInfrastructureSystemsTable } from "../hooks/useDiscoveredInfrastructureSystemsTable";

interface DiscoveredInfrastructureSystemsTableProps {
monitorId: string;
}

export const DiscoveredInfrastructureSystemsTable = ({
monitorId,
}: DiscoveredInfrastructureSystemsTableProps) => {
const {
// Table state and data
columns,
searchQuery,
updateSearch,

// Ant Design table integration
tableProps,
selectionProps,

// Tab management
filterTabs,
activeTab,
handleTabChange,

// Selection
selectedRows,
hasSelectedRows,
} = useDiscoveredInfrastructureSystemsTable({ monitorId });

return (
<>
<Menu
aria-label="Asset state filter"
mode="horizontal"
items={filterTabs.map((tab) => ({
key: tab.hash,
label: tab.label,
}))}
selectedKeys={[activeTab]}
onClick={async (menuInfo) => {
await handleTabChange(menuInfo.key as ActionCenterTabHash);
}}
className="mb-4"
data-testid="asset-state-filter"
/>
<Flex justify="space-between" align="center" className="mb-4">
<DebouncedSearchInput value={searchQuery} onChange={updateSearch} />
<Space size="large">
{hasSelectedRows && <SelectedText count={selectedRows.length} />}
</Space>
</Flex>
<Table {...tableProps} columns={columns} rowSelection={selectionProps} />
</>
);
};
50 changes: 50 additions & 0 deletions clients/admin-ui/src/mocks/action-center/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ConnectionType } from "~/types/api";

/**
* Mock Okta monitor for action center testing
*/
export const mockOktaMonitor = {
connection_type: ConnectionType.OKTA,
secrets: null,
saas_config: null,
name: "Okta Identity Provider",
key: "okta_identity_provider",
last_monitored: "2025-11-21T15:30:00.000000Z",
updates: {
unlabeled: 0,
in_review: 15,
classifying: 0,
removals: 0,
approved: 5,
classified_low_confidence: null,
classified_high_confidence: null,
},
total_updates: 20,
consent_status: null,
connection_name: "Okta Production",
has_failed_tasks: false,
};

/**
* Mock Okta app asset for system aggregate results
*/
export const mockOktaApp = {
urn: "urn:okta:app:12345678-1234-1234-1234-123456789012",
name: "Salesforce",
description: "Customer relationship management platform",
resource_type: "okta_app",
diff_status: "addition",
updated_at: "2024-01-15T10:30:00Z",
monitor_config_id: "okta_monitor_001",
system: "Okta Identity Provider",
metadata: {
app_type: "SAML_2_0",
status: "ACTIVE",
created: "2023-06-15T09:00:00Z",
sign_on_url: "https://company.okta.com/app/salesforce/sso/saml",
vendor_match_confidence: "medium",
vendor_logo_url: "https://logo.clearbit.com/salesforce.com",
},
vendor_id: "fds.1234",
system_id: "okta_system_001",
};
44 changes: 44 additions & 0 deletions clients/admin-ui/src/mocks/action-center/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable import/no-extraneous-dependencies */
import { rest } from "msw";

import { mockOktaApp, mockOktaMonitor } from "./data";

/**
* MSW handlers for discovery monitor aggregate results
*/
export const discoveryMonitorHandlers = () => {
const apiBase = "/api/v1";

return [
// GET - return mock Okta monitor
rest.get(
`${apiBase}/plus/discovery-monitor/aggregate-results`,
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
items: [mockOktaMonitor],
total: 1,
page: 1,
size: 25,
pages: 1,
}),
),
),
// GET - return mock Okta app assets for system aggregate results
rest.get(
`${apiBase}/plus/discovery-monitor/system-aggregate-results`,
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
items: [mockOktaApp],
total: 10,
page: 1,
size: 25,
pages: 1,
}),
),
),
];
};
4 changes: 3 additions & 1 deletion clients/admin-ui/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { taxonomyHandlers } from "~/features/taxonomy/taxonomy.mocks";

import { discoveryMonitorHandlers } from "./action-center/handlers";

// eslint-disable-next-line import/prefer-default-export
export const handlers = [...taxonomyHandlers()];
export const handlers = [...taxonomyHandlers(), ...discoveryMonitorHandlers()];
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextPage } from "next";
import { useRouter } from "next/router";

import FixedLayout from "~/features/common/FixedLayout";
import { ACTION_CENTER_ROUTE } from "~/features/common/nav/routes";
import PageHeader from "~/features/common/PageHeader";
import { DiscoveredInfrastructureSystemsTable } from "~/features/data-discovery-and-detection/action-center/tables/DiscoveredInfrastructureSystemsTable";

const MonitorResultSystems: NextPage = () => {
const router = useRouter();
const monitorId = decodeURIComponent(router.query.monitorId as string);

return (
<FixedLayout title="Action center - Discovered infrastructure systems">
<PageHeader
heading="Action center"
breadcrumbItems={[
{ title: "All activity", href: ACTION_CENTER_ROUTE },
{ title: monitorId },
]}
/>
<DiscoveredInfrastructureSystemsTable monitorId={monitorId} />
</FixedLayout>
);
};

export default MonitorResultSystems;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextPage } from "next";
import { useRouter } from "next/router";
import { useLayoutEffect } from "react";

import { ACTION_CENTER_ROUTE } from "~/features/common/nav/routes";

const DataExplorerRedirect: NextPage = () => {
const router = useRouter();

useLayoutEffect(() => {
router.replace(ACTION_CENTER_ROUTE);
}, [router]);

return null;
};

export default DataExplorerRedirect;
Loading