Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
33 changes: 29 additions & 4 deletions frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ import {
SecretImportTableRow,
SecretNoAccessTableRow,
SecretRotationTableRow,
SecretSyncStatusBadgeOverview,
SecretTableRow
} from "./components";

Expand Down Expand Up @@ -2562,6 +2563,9 @@ const OverviewPageContent = () => {
: "Click to enable batch edit mode. Changes will be grouped into a single commit."}
</TooltipContent>
</Tooltip>
<SecretSyncStatusBadgeOverview
environmentSlugs={visibleEnvs.map((e) => e.slug)}
/>
</div>
)}
<SecretDropzone
Expand All @@ -2579,7 +2583,12 @@ const OverviewPageContent = () => {
<UnstableTable ref={tableRef} className="border-separate border-spacing-0">
<UnstableTableHeader>
<UnstableTableRow className="h-10">
<UnstableTableHead className="sticky left-0 z-10 w-[40px] max-w-[40px] min-w-[40px] bg-container">
<UnstableTableHead
className={twMerge(
!isSingleEnvView && "sticky",
"left-0 z-10 w-[40px] max-w-[40px] min-w-[40px] bg-container"
)}
>
<Checkbox
variant="project"
isDisabled={totalCount === 0 || hasPendingBatchChanges}
Expand All @@ -2590,7 +2599,10 @@ const OverviewPageContent = () => {
/>
</UnstableTableHead>
<UnstableTableHead
className="sticky left-10 z-10 max-w-60 min-w-60 border-r bg-container lg:max-w-none lg:min-w-96"
className={twMerge(
!isSingleEnvView && "sticky",
"left-10 z-10 max-w-60 min-w-60 border-r bg-container lg:max-w-none lg:min-w-96"
)}
onClick={() =>
setOrderDirection((prev) =>
prev === OrderByDirection.ASC
Expand Down Expand Up @@ -2799,6 +2811,9 @@ const OverviewPageContent = () => {
: "Click to enable batch edit mode. Changes will be grouped into a single commit."}
</TooltipContent>
</Tooltip>
<SecretSyncStatusBadgeOverview
environmentSlugs={visibleEnvs.map((e) => e.slug)}
/>
</div>
</div>
</UnstableTableHead>
Expand All @@ -2809,10 +2824,20 @@ const OverviewPageContent = () => {
{isOverviewLoading || isPlaceholderData ? (
Array.from({ length: prevPageSize.current || perPage }).map((_, index) => (
<UnstableTableRow className="group" key={`loading-row-${index + 1}`}>
<UnstableTableCell className="sticky left-0 z-10 bg-container group-hover:bg-container-hover">
<UnstableTableCell
className={twMerge(
!isSingleEnvView && "sticky",
"left-0 z-10 bg-container group-hover:bg-container-hover"
)}
>
<Skeleton className="h-4 w-full" />
</UnstableTableCell>
<UnstableTableCell className="sticky left-10 z-10 border-r bg-container group-hover:bg-container-hover">
<UnstableTableCell
className={twMerge(
!isSingleEnvView && "sticky",
"left-10 z-10 border-r bg-container group-hover:bg-container-hover"
)}
>
<Skeleton className="h-4 w-full" />
</UnstableTableCell>
{visibleEnvs.map((env) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useMemo } from "react";
import { useNavigate, useParams } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import { AlertTriangleIcon, CheckIcon, RefreshCwIcon } from "lucide-react";

import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@app/components/v3";
import { ROUTE_PATHS } from "@app/const/routes";
import { useProject } from "@app/context";
import { SecretSyncStatus, useListSecretSyncs } from "@app/hooks/api/secretSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";

type BadgeState = {
variant: "neutral" | "danger" | "success";
label: string;
Icon: typeof RefreshCwIcon;
isAnimated: boolean;
tooltipTitle: string;
};

type Props = {
environmentSlugs?: string[];
};

export const SecretSyncStatusBadgeOverview = ({ environmentSlugs }: Props) => {
const navigate = useNavigate();
const { projectId } = useProject();
const orgId = useParams({
from: ROUTE_PATHS.SecretManager.OverviewPage.id,
select: (el) => el.orgId
});

const { data: secretSyncs = [] } = useListSecretSyncs(projectId, {
refetchInterval: 20_000
});

const filteredSyncs = useMemo(() => {
if (!environmentSlugs || environmentSlugs.length === 0) return secretSyncs;
return secretSyncs.filter(
(s) => s.environment && environmentSlugs.includes(s.environment.slug)
);
}, [secretSyncs, environmentSlugs]);

const { runningSyncs, failedSyncs, succeededSyncs } = useMemo(() => {
const running = filteredSyncs.filter(
(s) => s.syncStatus === SecretSyncStatus.Running || s.syncStatus === SecretSyncStatus.Pending
);
const failed = filteredSyncs.filter((s) => s.syncStatus === SecretSyncStatus.Failed);
const succeeded = filteredSyncs.filter((s) => s.syncStatus === SecretSyncStatus.Succeeded);
return { runningSyncs: running, failedSyncs: failed, succeededSyncs: succeeded };
}, [filteredSyncs]);

const badgeState = useMemo((): BadgeState | null => {
if (filteredSyncs.length === 0) return null;

if (runningSyncs.length > 0) {
return {
variant: "neutral",
label: `Syncing (${runningSyncs.length})`,
Icon: RefreshCwIcon,
isAnimated: true,
tooltipTitle: "Currently Syncing"
};
}

if (failedSyncs.length > 0) {
return {
variant: "danger",
label: `Sync Failed (${failedSyncs.length})`,
Icon: AlertTriangleIcon,
isAnimated: false,
tooltipTitle: "Failed Syncs"
};
}

if (succeededSyncs.length > 0) {
return {
variant: "success",
label: "Synced",
Icon: CheckIcon,
isAnimated: false,
tooltipTitle: "All Syncs Succeeded"
};
}

return null;
}, [filteredSyncs, runningSyncs, failedSyncs, succeededSyncs]);

const displayedSyncs = useMemo(() => {
if (runningSyncs.length > 0) return runningSyncs;
if (failedSyncs.length > 0) return failedSyncs;
return succeededSyncs;
}, [runningSyncs, failedSyncs, succeededSyncs]);

if (!badgeState) return null;

const { variant, label, Icon, isAnimated, tooltipTitle } = badgeState;

const handleNavigateToSyncs = (e: React.MouseEvent) => {
e.stopPropagation();
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
params: { orgId, projectId },
search: { selectedTab: IntegrationsListPageTabs.SecretSyncs }
});
};

return (
<Tooltip>
<TooltipTrigger asChild>
<Badge asChild variant={variant}>
<button type="button" onClick={handleNavigateToSyncs}>
<Icon className={isAnimated ? "animate-spin" : ""} />
{label}
</button>
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<div className="flex flex-col gap-1.5 py-1">
<span className="text-xs font-medium">{tooltipTitle}</span>
{displayedSyncs.slice(0, 8).map((sync) => (
<div key={sync.id} className="flex items-center gap-2 text-xs">
<span className="truncate">{sync.name}</span>
{sync.lastSyncedAt && (
<span className="ml-auto shrink-0 text-accent">
{formatDistanceToNow(new Date(sync.lastSyncedAt), { addSuffix: true })}
</span>
)}
</div>
))}
{displayedSyncs.length > 8 && (
<span className="text-xs text-accent">and {displayedSyncs.length - 8} more...</span>
)}
</div>
</TooltipContent>
</Tooltip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from "./ResourceSearchInput";
export * from "./SecretDropzone";
export * from "./SecretImportTableRow";
export * from "./SecretRotationTableRow";
export * from "./SecretSyncStatusBadgeOverview";
export * from "./SecretTableRow";
Loading