diff --git a/front/components/members/InvitationsList.tsx b/front/components/members/InvitationsList.tsx index 72a939a82a6b..db9251588090 100644 --- a/front/components/members/InvitationsList.tsx +++ b/front/components/members/InvitationsList.tsx @@ -4,6 +4,8 @@ import { normalizeDisplayRole, ROLES_DATA, } from "@app/components/members/Roles"; +import type { UsageTableSkeletonCellProps } from "@app/components/workspace/UsageTableSkeleton"; +import { UsageTableSkeleton } from "@app/components/workspace/UsageTableSkeleton"; import { useSendNotification } from "@app/hooks/useNotification"; import { sendInvitations } from "@app/lib/invitations"; import { useWorkspaceInvitations } from "@app/lib/swr/memberships"; @@ -13,8 +15,9 @@ import { isAdmin } from "@app/types/user"; import { Button, Chip, + cn, DataTable, - DataTableLoadingSkeleton, + LoadingBlock, Mail01, Page, } from "@dust-tt/sparkle"; @@ -26,6 +29,27 @@ type RowData = MembershipInvitationType & { onClick: () => void; }; +function InvitationSkeletonCell({ + columnId, + rowIndex, +}: UsageTableSkeletonCellProps) { + switch (columnId) { + case "inviteEmail": + return ( + + ); + case "initialRole": + return ; + default: + return null; + } +} + export function InvitationsList({ owner, searchText, @@ -143,7 +167,11 @@ export function InvitationsList({ />
{isInvitationsLoading && ( - + )} {!isInvitationsLoading && invitations.length === 0 && (
diff --git a/front/components/pages/workspace/developers/SecretsPage.tsx b/front/components/pages/workspace/developers/SecretsPage.tsx index 775a283800bd..1ad5848f1314 100644 --- a/front/components/pages/workspace/developers/SecretsPage.tsx +++ b/front/components/pages/workspace/developers/SecretsPage.tsx @@ -1,4 +1,6 @@ import { AdminPageContainer } from "@app/components/layouts/AdminPageContainer"; +import type { UsageTableSkeletonCellProps } from "@app/components/workspace/UsageTableSkeleton"; +import { UsageTableSkeleton } from "@app/components/workspace/UsageTableSkeleton"; import { useSendNotification } from "@app/hooks/useNotification"; import { useAuth, useWorkspace } from "@app/lib/auth/AuthContext"; import { useSubmitFunction } from "@app/lib/client/utils"; @@ -8,8 +10,8 @@ import type { DustAppSecretType } from "@app/types/dust_app_secret"; import { BookOpen01, Button, + cn, DataTable, - DataTableLoadingSkeleton, Dialog, DialogContainer, DialogContent, @@ -18,6 +20,7 @@ import { DialogTitle, Edit04, Input, + LoadingBlock, Page, Plus, SearchInput, @@ -337,6 +340,31 @@ interface SecretsTableProps { searchQuery: string; } +function SecretSkeletonCell({ + columnId, + rowIndex, +}: UsageTableSkeletonCellProps) { + switch (columnId) { + case "name": + return ( +
+ + +
+ ); + case "actions": + // Edit and delete only appear on hover in the loaded table. + return null; + default: + return null; + } +} + function SecretsTable({ isLoading, isError, @@ -344,7 +372,9 @@ function SecretsTable({ searchQuery, }: SecretsTableProps) { if (isLoading) { - return ; + return ( + + ); } if (isError) { diff --git a/front/components/skills/import/DetectedSkillsList.tsx b/front/components/skills/import/DetectedSkillsList.tsx index 30aa748cd09d..bf25ed50f51f 100644 --- a/front/components/skills/import/DetectedSkillsList.tsx +++ b/front/components/skills/import/DetectedSkillsList.tsx @@ -7,10 +7,11 @@ import { isImportableSkillStatus } from "@app/lib/skill_detection"; import { Chip, ContentMessage, + cn, createSelectionColumn, DataTable, - DataTableLoadingSkeleton, InfoCircle, + LoadingBlock, PuzzlePiece01, ScrollableDataTable, } from "@dust-tt/sparkle"; @@ -36,6 +37,34 @@ interface SkillRowData { type SkillCellInfo = CellContext; +const DETECTED_SKILLS_SKELETON_ROWS: SkillRowData[] = Array.from( + { length: 3 }, + () => ({ name: "", status: "ready" }) +); + +function renderDetectedSkillSkeletonCell(columnId: string, rowIndex: number) { + switch (columnId) { + case "select": + return ; + case "name": + return ( +
+ + +
+ ); + case "status": + return ; + default: + return null; + } +} + function getColumns(): ColumnDef[] { return [ createSelectionColumn(), @@ -103,6 +132,24 @@ export function DetectedSkillsList({ ); const columns = useMemo(() => getColumns(), []); + const skeletonColumns = useMemo( + () => + columns.map((column) => { + const skeletonColumn = { + ...column, + cell: (info: SkillCellInfo) => + renderDetectedSkillSkeletonCell(info.column.id, info.row.index), + }; + return column.id === "select" + ? { + ...skeletonColumn, + id: "select", + header: () => , + } + : skeletonColumn; + }), + [columns] + ); // Build rowSelection state from selectedSkillNames form field. const rowSelection = useMemo(() => { @@ -131,7 +178,17 @@ export function DetectedSkillsList({ return ( <> - {isDetecting && } + {isDetecting && ( +
+ +
+ )} {detectError && ( { - columns: ColumnDef[]; +interface UsageTableSkeletonProps { + columns: ColumnDef[]; SkeletonCell: ComponentType; rowCount?: number; rowHeight?: number; } -export function UsageTableSkeleton({ +/** + * @cc [owner:aubin-tchoi,label:react] content-specific-skeleton-cells + * Callers MUST reuse their loaded table's column definitions and provide a + * SkeletonCell that matches each column's content shape, including icons, + * avatars, text lines, and controls where present. + */ +export function UsageTableSkeleton({ columns, SkeletonCell, rowCount = 5, rowHeight = 48, -}: UsageTableSkeletonProps) { +}: UsageTableSkeletonProps) { const table = useReactTable({ data: [], columns, diff --git a/front/components/workspace/analytics/automations/SlackWorkflowsTab.tsx b/front/components/workspace/analytics/automations/SlackWorkflowsTab.tsx index 58913ad54afb..67adaed89c78 100644 --- a/front/components/workspace/analytics/automations/SlackWorkflowsTab.tsx +++ b/front/components/workspace/analytics/automations/SlackWorkflowsTab.tsx @@ -1,6 +1,8 @@ import { ConfirmContext } from "@app/components/Confirm"; import { AllowSlackWorkflowDialog } from "@app/components/workspace/analytics/automations/AllowSlackWorkflowDialog"; import { SummaryCard } from "@app/components/workspace/analytics/SummaryCard"; +import type { UsageTableSkeletonCellProps } from "@app/components/workspace/UsageTableSkeleton"; +import { UsageTableSkeleton } from "@app/components/workspace/UsageTableSkeleton"; import { useSlackWorkflowsOverview } from "@app/hooks/useSlackWorkflowsOverview"; import type { ConsumptionPeriodSelection } from "@app/lib/analytics/consumption_period"; import { formatCredits } from "@app/lib/client/credits"; @@ -14,8 +16,8 @@ import { GLOBAL_SPACE_NAME } from "@app/types/groups"; import type { LightWorkspaceType } from "@app/types/user"; import { Button, + cn, DataTable, - DataTableLoadingSkeleton, LoadingBlock, Plus, SearchInput, @@ -271,6 +273,40 @@ interface SlackWorkflowsTableBodyProps { setPagination: (pagination: PaginationState) => void; } +function SlackWorkflowSkeletonCell({ + columnId, + rowIndex, +}: UsageTableSkeletonCellProps) { + switch (columnId) { + case "botName": + return ( + + ); + case "spaceNames": + return ( + + ); + case "createdAt": + return ; + case "revoke": + return ( + + ); + default: + return null; + } +} + function SlackWorkflowsTableBody({ columns, rows, @@ -282,7 +318,18 @@ function SlackWorkflowsTableBody({ }: SlackWorkflowsTableBodyProps) { if (isLoading) { return ( - +
+ + +
); } diff --git a/front/components/workspace/api-keys/APIKeysTable.tsx b/front/components/workspace/api-keys/APIKeysTable.tsx index 791970e79d40..295e7c37b973 100644 --- a/front/components/workspace/api-keys/APIKeysTable.tsx +++ b/front/components/workspace/api-keys/APIKeysTable.tsx @@ -1,3 +1,5 @@ +import type { UsageTableSkeletonCellProps } from "@app/components/workspace/UsageTableSkeleton"; +import { UsageTableSkeleton } from "@app/components/workspace/UsageTableSkeleton"; import { useConsumptionTop } from "@app/hooks/useConsumptionTop"; import type { ConsumptionPeriodSelection } from "@app/lib/analytics/consumption_period"; import { formatCredits } from "@app/lib/client/credits"; @@ -16,8 +18,8 @@ import { ChevronLeft, ChevronRight, Chip, + cn, DataTable, - DataTableLoadingSkeleton, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, @@ -30,7 +32,6 @@ import { LoadingBlock, Lock01, SearchInput, - Separator, Tooltip, Trash01, } from "@dust-tt/sparkle"; @@ -46,6 +47,57 @@ import { useMemo, useState } from "react"; const API_KEYS_PAGE_SIZE = 10; const MAX_API_KEY_CONSUMPTION_ROWS = 100; +function APIKeySkeletonCell({ + columnId, + rowIndex, +}: UsageTableSkeletonCellProps) { + switch (columnId) { + case "name": + return ( +
+
+ +
+
+ +
+
+ ); + case "scope": + return ; + case "key": + return ; + case "spaces": + return ; + case "credits": + return ; + case "monthlyCap": + return ; + case "lastUsedAt": + return ; + case "status": + return ; + case "revoke": + return ( + + ); + case "actions": + return ; + default: + return null; + } +} + type APIKeyStatus = "active" | "capped" | "revoked"; interface APIKeysTableProps { @@ -698,13 +750,12 @@ export function APIKeysTable({ {isLoading || isSpacesLoading ? ( <> - - -
+