feature: secret scanning architecture and github data source (wip)

This commit is contained in:
Scott Wilson
2025-05-28 22:21:03 -07:00
parent 931ee1e8da
commit e6c97510ca
231 changed files with 11443 additions and 1048 deletions
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SecretScanningDataSourcesSection } from "./components";
export const SecretScanningDataSourcesPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Data Sources"
description="Manage your Secret Scanning data sources."
/>
<SecretScanningDataSourcesSection />
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,327 @@
import { useCallback } from "react";
import {
faBan,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faExpand,
faInfoCircle,
faSearch,
faToggleOff,
faToggleOn,
faTrash,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretScanningScanStatusBadge } from "@app/components/secret-scanning";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
import {
RESOURCE_DESCRIPTION_HELPER,
SECRET_SCANNING_DATA_SOURCE_MAP
} from "@app/helpers/secretScanningV2";
import { useToggle } from "@app/hooks";
import {
SecretScanningFindingStatus,
SecretScanningScanStatus,
TSecretScanningDataSourceWithDetails
} from "@app/hooks/api/secretScanningV2";
type Props = {
dataSource: TSecretScanningDataSourceWithDetails;
onDelete: (dataSource: TSecretScanningDataSourceWithDetails) => void;
onTriggerScan: (dataSource: TSecretScanningDataSourceWithDetails) => void;
onToggleEnableAutoScan: (dataSource: TSecretScanningDataSourceWithDetails) => void;
onEdit: (dataSource: TSecretScanningDataSourceWithDetails) => void;
};
export const SecretScanningDataSourceRow = ({
dataSource,
onDelete,
onEdit,
onTriggerScan,
onToggleEnableAutoScan
}: Props) => {
const navigate = useNavigate();
const {
id,
name,
description,
isAutoScanEnabled,
projectId,
type,
unresolvedFindings,
lastScannedAt,
lastScanStatus,
lastScanStatusMessage
} = dataSource;
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
const [isIdCopied, setIsIdCopied] = useToggle(false);
const handleCopyId = useCallback(() => {
setIsIdCopied.on();
navigator.clipboard.writeText(id);
createNotification({
text: "Data Source ID copied to clipboard",
type: "info"
});
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isIdCopied]);
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
return (
<Tr
onClick={() =>
navigate({
to: ROUTE_PATHS.SecretScanning.DataSourceByIdPage.path,
params: {
dataSourceId: id,
type,
projectId
}
})
}
className={twMerge(
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
lastScanStatus === SecretScanningScanStatus.Failed && "bg-red/5 hover:bg-red/10"
)}
key={`data-source-${id}`}
>
<Td className="!min-w-[8rem] max-w-0">
<div className="flex w-full items-center">
<img
alt={`${sourceDetails.name} Data Source`}
src={`/images/integrations/${sourceDetails.image}`}
className="w-5"
/>
<p className="ml-2 truncate">{sourceDetails.name}</p>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
<div className="flex w-full items-center">
<p className="truncate">{name}</p>
{description && (
<Tooltip content={description}>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="ml-1 text-mineshaft-400" />
</Tooltip>
)}
</div>
</Td>
<Td>
{/* eslint-disable-next-line no-nested-ternary */}
{lastScanStatus?.match(/queued|scanning/) ? (
<Badge
variant="primary"
className="flex h-5 w-min animate-pulse items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300"
>
<FontAwesomeIcon icon={faSearch} />
<span>Scanning For Leaks</span>
</Badge>
) : // eslint-disable-next-line no-nested-ternary
lastScannedAt ? (
// eslint-disable-next-line no-nested-ternary
unresolvedFindings ? (
<Badge
variant="primary"
className="flex h-5 w-min cursor-pointer items-center gap-1.5 whitespace-nowrap"
onClick={(e) => {
e.stopPropagation();
navigate({
to: "/secret-scanning/$projectId/findings",
params: {
projectId
},
search: {
search: name,
status: SecretScanningFindingStatus.Unresolved
}
});
}}
>
<FontAwesomeIcon icon={faWarning} />
<span>
{unresolvedFindings} Secret{unresolvedFindings > 1 ? "s" : ""} Detected
</span>
</Badge>
) : lastScanStatus === SecretScanningScanStatus.Failed ? (
<span className="text-mineshaft-400">No findings</span>
) : (
<Badge
variant="success"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faCheck} />
No Secrets Detected
</Badge>
)
) : (
<span className="text-mineshaft-400">No findings</span>
)}
</Td>
<Td>
<div className="flex w-full items-center gap-1">
{/* eslint-disable-next-line no-nested-ternary */}
{lastScanStatus?.match(/queued|scanning|failed/) ? (
<SecretScanningScanStatusBadge
status={lastScanStatus}
statusMessage={lastScanStatusMessage}
scannedAt={lastScannedAt}
/>
) : lastScannedAt ? (
<span>{formatDistance(new Date(lastScannedAt), new Date(), { addSuffix: true })}</span>
) : (
<span className="text-mineshaft-400">No scans</span>
)}
{!isAutoScanEnabled && (
<Tooltip
className="text-xs"
content={`Auto-Scan is disabled. Scans will not be automatically triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source`}
>
<div className="ml-auto">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Auto-Scan Disabled</span>
</Badge>
</div>
</Tooltip>
)}
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Data Source ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
a={ProjectPermissionSub.SecretScanningDataSources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faExpand} />}
onClick={(e) => {
e.stopPropagation();
onTriggerScan(dataSource);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Manually trigger a scan for this ${sourceDetails.name} data source.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span> Trigger Scan</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
a={ProjectPermissionSub.SecretScanningDataSources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isAutoScanEnabled ? faToggleOff : faToggleOn} />}
onClick={(e) => {
e.stopPropagation();
onToggleEnableAutoScan(dataSource);
}}
>
{isAutoScanEnabled ? "Disable" : "Enable"} Auto-Scan
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
a={ProjectPermissionSub.SecretScanningDataSources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={(e) => {
e.stopPropagation();
onEdit(dataSource);
}}
>
Edit Data Source
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningDataSourceActions.Delete}
a={ProjectPermissionSub.SecretScanningDataSources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(dataSource);
}}
>
Delete Data Source
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
};
@@ -0,0 +1,82 @@
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning";
import { Button, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourcesTable } from "./SecretScanningDataSourcesTable";
export const SecretScanningDataSourcesSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addDataSource"] as const);
const { currentWorkspace } = useWorkspace();
const { data: dataSources = [], isPending: isDataSourcesPending } =
useListSecretScanningDataSources(currentWorkspace.id, {
refetchInterval: 30000
});
if (isDataSourcesPending)
return (
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
<Spinner />
</div>
);
return (
<>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Data Sources</p>
<a
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Use App Connections to scan for secret leaks from third-party services.
</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningDataSourceActions.Create}
a={ProjectPermissionSub.SecretScanningDataSources}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addDataSource")}
isDisabled={!isAllowed}
>
Add Data Source
</Button>
)}
</ProjectPermissionCan>
</div>
<SecretScanningDataSourcesTable dataSources={dataSources} />
</div>
<CreateSecretScanningDataSourceModal
isOpen={popUp.addDataSource.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addDataSource", isOpen)}
/>
</>
);
};
@@ -0,0 +1,387 @@
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPuzzlePiece,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
import { DeleteSecretScanningDataSourceModal } from "@app/components/secret-scanning/DeleteSecretScanningDataSourceModal";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
SecretScanningDataSource,
SecretScanningScanStatus,
TSecretScanningDataSource,
TSecretScanningDataSourceWithDetails,
useTriggerSecretScanningDataSource,
useUpdateSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
// import { getSecretSyncDestinationColValues } from "./helpers";
enum DataSourcesOrderBy {
Findings = "findings",
Name = "name",
LastScan = "last-scan"
}
type DataSourceFilters = {
types: SecretScanningDataSource[];
};
type Props = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteDataSource",
"editDataSource",
"triggerDataSourceScan"
] as const);
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const [filters, setFilters] = useState<DataSourceFilters>({
types: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<DataSourcesOrderBy>(DataSourcesOrderBy.Name, { initPerPage: 20 });
const filteredDataSources = useMemo(
() =>
dataSources
.filter((dataSource) => {
const { type, name, connection } = dataSource;
if (filters.types.length && !filters.types.includes(type)) return false;
const searchValue = search.trim().toLowerCase();
return (
SECRET_SCANNING_DATA_SOURCE_MAP[type].name.toLowerCase().includes(searchValue) ||
name.toLowerCase().includes(searchValue) ||
connection.name.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [dataSourceOne, dataSourceTwo] =
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case DataSourcesOrderBy.LastScan:
if (dataSourceOne.lastScannedAt === null && dataSourceTwo.lastScannedAt === null)
return 0;
if (dataSourceOne.lastScannedAt === null) return 1;
if (dataSourceTwo.lastScannedAt === null) return -1;
return (
new Date(dataSourceTwo.lastScannedAt).getTime() -
new Date(dataSourceOne.lastScannedAt).getTime()
);
case DataSourcesOrderBy.Findings:
if (
dataSourceOne.unresolvedFindings === null &&
dataSourceTwo.unresolvedFindings === null
)
return 0;
if (dataSourceOne.unresolvedFindings === null) return 1;
if (dataSourceTwo.unresolvedFindings === null) return -1;
if (
dataSourceOne.unresolvedFindings === 0 &&
dataSourceOne.lastScanStatus === SecretScanningScanStatus.Failed
)
return 1;
if (
dataSourceTwo.unresolvedFindings === 0 &&
dataSourceTwo.lastScanStatus === SecretScanningScanStatus.Failed
)
return -1;
return dataSourceTwo.unresolvedFindings - dataSourceOne.unresolvedFindings;
case DataSourcesOrderBy.Name:
default:
return dataSourceOne.name
.toLowerCase()
.localeCompare(dataSourceTwo.name.toLowerCase());
}
}),
[dataSources, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredDataSources.length,
offset,
setPage
});
const handleSort = (column: DataSourcesOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: DataSourcesOrderBy) =>
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: DataSourcesOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.types.length);
const handleDelete = (dataSource: TSecretScanningDataSource) =>
handlePopUpOpen("deleteDataSource", dataSource);
const handleEdit = (dataSource: TSecretScanningDataSource) =>
handlePopUpOpen("editDataSource", dataSource);
const handleToggleEnableAutoScan = async (dataSource: TSecretScanningDataSource) => {
const destinationName = SECRET_SCANNING_DATA_SOURCE_MAP[dataSource.type].name;
const isAutoScanEnabled = !dataSource.isAutoScanEnabled;
try {
await updateDataSource.mutateAsync({
dataSourceId: dataSource.id,
type: dataSource.type,
isAutoScanEnabled,
projectId: dataSource.projectId
});
createNotification({
text: `Successfully ${isAutoScanEnabled ? "enabled" : "disabled"} auto-scan for ${destinationName} Data Source`,
type: "success"
});
} catch {
createNotification({
text: `Failed to ${isAutoScanEnabled ? "enable" : "disable"} auto-scan for ${destinationName} Data Source`,
type: "error"
});
}
};
const handleTriggerScan = async (dataSource: TSecretScanningDataSource) => {
try {
await triggerDataSourceScan.mutateAsync({
dataSourceId: dataSource.id,
type: dataSource.type,
projectId: dataSource.projectId
});
createNotification({
text: "Successfully triggered scan",
type: "success"
});
} catch {
createNotification({
text: "Failed to trigger scan",
type: "error"
});
}
};
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search data sources..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter data sources"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Platform</DropdownMenuLabel>
{dataSources.length ? (
[...new Set(dataSources.map(({ type }) => type))].map((type) => {
const { name, image } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
types: prev.types.includes(type)
? prev.types.filter((a) => a !== type)
: [...prev.types, type]
}));
}}
key={type}
icon={
filters.types.includes(type) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} integration`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Data Sources Configured</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-60">Platform</Th>
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(DataSourcesOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(DataSourcesOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Findings
<IconButton
variant="plain"
className={getClassName(DataSourcesOrderBy.Findings)}
ariaLabel="sort"
onClick={() => handleSort(DataSourcesOrderBy.Findings)}
>
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.Findings)} />
</IconButton>
</div>
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Last Scan
<IconButton
variant="plain"
className={getClassName(DataSourcesOrderBy.LastScan)}
ariaLabel="sort"
onClick={() => handleSort(DataSourcesOrderBy.LastScan)}
>
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.LastScan)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{filteredDataSources.slice(offset, perPage * page).map((dataSource) => (
<SecretScanningDataSourceRow
key={dataSource.id}
dataSource={dataSource}
onDelete={handleDelete}
onEdit={handleEdit}
onTriggerScan={handleTriggerScan}
onToggleEnableAutoScan={handleToggleEnableAutoScan}
/>
))}
</TBody>
</Table>
{Boolean(filteredDataSources.length) && (
<Pagination
count={filteredDataSources.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{!filteredDataSources?.length && (
<EmptyState
title={
dataSources.length
? "No data sources match search..."
: "This project has no data sources configured"
}
icon={dataSources.length ? faSearch : faPuzzlePiece}
/>
)}
</TableContainer>
<DeleteSecretScanningDataSourceModal
onOpenChange={(isOpen) => handlePopUpToggle("deleteDataSource", isOpen)}
isOpen={popUp.deleteDataSource.isOpen}
dataSource={popUp.deleteDataSource.data}
/>
<EditSecretScanningDataSourceModal
onOpenChange={(isOpen) => handlePopUpToggle("editDataSource", isOpen)}
isOpen={popUp.editDataSource.isOpen}
dataSource={popUp.editDataSource.data}
/>
</div>
);
};
@@ -0,0 +1 @@
export { SecretScanningDataSourcesSection } from "./SecretScanningDataSourcesSection";
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/"
)({
component: SecretScanningDataSourcesPage
// beforeLoad: ({ context }) => {
// return {
// breadcrumbs: [
// ...context.breadcrumbs,
// {
// label: "Data Sources"
// }
// ]
// };
// }
});