)}
{!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 ? (
<>
-
-
-
+
diff --git a/sparkle/src/components/DataTableLoadingSkeleton.tsx b/sparkle/src/components/DataTableLoadingSkeleton.tsx
deleted file mode 100644
index 5292b55aa6d2..000000000000
--- a/sparkle/src/components/DataTableLoadingSkeleton.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { cn } from "@sparkle/lib/utils";
-import * as React from "react";
-
-import { LoadingBlock } from "./LoadingBlock";
-
-const LABEL_WIDTHS = ["w-32", "w-48", "w-28", "w-40", "w-36"];
-
-interface DataTableLoadingSkeletonProps
- extends React.HTMLAttributes
{
- /** Number of placeholder rows to render (default 5). */
- rows?: number;
- /** Includes a leading checkbox-shaped block, matching a selectable table (default true). */
- showSelectionColumn?: boolean;
- /** Includes a trailing pill-shaped block, matching a table with a trailing cell. */
- showTrailingCell?: boolean;
-}
-
-/**
- * A skeleton placeholder mirroring the layout of a DataTable /
- * ScrollableDataTable list, built on LoadingBlock, so the swap to real
- * content feels seamless. Use it while loading rows whose shape is known
- * ahead of time; for an indeterminate load with no known layout, use a
- * Spinner instead.
- * @summary Loading placeholder shaped like table rows.
- */
-function DataTableLoadingSkeleton({
- rows = 5,
- showSelectionColumn = true,
- showTrailingCell = false,
- className,
- ...props
-}: DataTableLoadingSkeletonProps) {
- return (
-
-
- {showSelectionColumn && (
-
-
-
- )}
-
-
-
- {showTrailingCell && (
-
-
-
- )}
-
- {Array.from({ length: rows }, (_, i) => (
-
- {showSelectionColumn && (
-
-
-
- )}
-
-
-
- {showTrailingCell && (
-
-
-
- )}
-
- ))}
-
- );
-}
-
-export { DataTableLoadingSkeleton };
diff --git a/sparkle/src/components/index.ts b/sparkle/src/components/index.ts
index 8b63d7d64c53..46e701a86bb1 100644
--- a/sparkle/src/components/index.ts
+++ b/sparkle/src/components/index.ts
@@ -100,7 +100,6 @@ export {
DataTable,
ScrollableDataTable,
} from "./DataTable";
-export { DataTableLoadingSkeleton } from "./DataTableLoadingSkeleton";
export {
Dialog,
DialogClose,
diff --git a/sparkle/src/stories/DataTableLoadingSkeleton.stories.tsx b/sparkle/src/stories/DataTableLoadingSkeleton.stories.tsx
deleted file mode 100644
index 908b373e8896..000000000000
--- a/sparkle/src/stories/DataTableLoadingSkeleton.stories.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-
-import { DataTableLoadingSkeleton } from "@sparkle/components";
-
-const meta = {
- title: "Feedback & Status/DataTableLoadingSkeleton",
- component: DataTableLoadingSkeleton,
- parameters: {
- docs: {
- description: {
- component: `A skeleton placeholder that mirrors the layout of a **DataTable** / **ScrollableDataTable** (header and rows with an optional selection checkbox, a label, and an optional trailing cell). Built on **LoadingBlock**, it reserves the table's shape while rows are being fetched, so the swap to real content feels seamless.
-
-**When to use**
-- While loading rows whose shape is known ahead of time (selectable lists, tables).
-
-**Guidelines**
-- Toggle \`showSelectionColumn\` and \`showTrailingCell\` to match the real table's columns.
-- Treat this as a reasonable default. Since cells can render arbitrary custom components, prefer a table-specific skeleton when you need the loading state to match their exact shape.
-- For an indeterminate load with no known layout, use a **Spinner** instead.`,
- },
- },
- },
- argTypes: {
- rows: { control: { type: "number" } },
- showSelectionColumn: { control: "boolean" },
- showTrailingCell: { control: "boolean" },
- },
-} satisfies Meta;
-
-export default meta;
-
-type Story = StoryObj;
-
-/**
- * Loading placeholder for a table whose rows carry a selection checkbox —
- * the shape of a selectable DataTable list mid-fetch.
- * @summary Loading rows with a selection column.
- */
-export const SelectableRowsLoading: Story = {
- args: {
- rows: 5,
- showSelectionColumn: true,
- showTrailingCell: false,
- },
-};
-
-/**
- * Loading placeholder for a plain, non-selectable list: label-only rows with
- * no checkbox and no trailing cell.
- * @summary Loading rows for a plain list.
- */
-export const PlainList: Story = {
- args: {
- rows: 6,
- showSelectionColumn: false,
- showTrailingCell: false,
- },
-};