Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1652496
feat(rollout): scaffold rollout framework presentational layer + Stor…
Jul 31, 2026
a86382c
refactor(rollout): align components with existing proto-fleet patterns
Jul 31, 2026
a13c33c
feat(rollout): auto-retry model, step-led status, view-rollout modal,…
Jul 31, 2026
e49c7f7
fix(rollout): ViewRolloutModal frames as a real modal (title bar + cl…
Jul 31, 2026
243f737
feat(rollout): real config modal + top-bar CTAs + scroll for both modals
Jul 31, 2026
ffb6b33
refactor(rollout): use standard-width modals
Jul 31, 2026
3f549f9
feat(rollout): pin modal header in top bar; pill + banners open the m…
Aug 3, 2026
fc686e2
refactor(rollout): simplify active-card bar/key, reposition scope, ma…
Aug 3, 2026
9d13b22
fix(rollout): pad the status icon below the modal's sticky top bar
Aug 3, 2026
481ddaa
fix(rollout): match embedded card top padding to the modal side margins
Aug 3, 2026
8c74f47
fix(rollout): make active-card stat grid responsive to container width
Aug 3, 2026
de34ea4
feat(rollout): add page-context in-situ stories (firmware settings, e…
Aug 3, 2026
88d7d0c
feat(rollout): wrap in-situ page stories in the real navigation shell
Aug 3, 2026
f1c2593
refactor(rollout): align column state with the shipped DeviceStatus/M…
Aug 3, 2026
daf6d0e
feat(rollout): seed nav permissions + dummy page content in in-situ s…
Aug 3, 2026
3f37bec
feat(rollout): story for FirmwareUpdateModal with rollout controls (o…
Aug 3, 2026
53d70fb
fix(rollout): use shipped table components + shared List in in-situ s…
Aug 3, 2026
da5e13d
chore(rollout): remove redundant standalone stories covered by In Situ
Aug 3, 2026
f826e6b
chore(rollout): drop incomplete fleet-view In Situ stories
Aug 3, 2026
6209661
refactor(rollout): consolidate stories into In Situ + Components buckets
Aug 3, 2026
0ddc172
refactor(rollout): split In Situ into Config + In Progress buckets
Aug 3, 2026
6bf7c0d
refactor(rollout): 2-up controls; align active-rollout card with curt…
Aug 4, 2026
f3da486
feat(rollout): add Manage CTA and excluded-count legend annotation to…
Aug 4, 2026
4672aff
chore(rollout): drop parenthetical qualifiers from story names
Aug 4, 2026
4be746d
feat(rollout): add per-state + animated lifecycle stories for firmwar…
Aug 4, 2026
66a7922
refactor(rollout): render firmware and reboot lifecycle stories in situ
Aug 4, 2026
a0f5aa6
refactor(rollout): render firmware + reboot lifecycle stories on the …
Aug 4, 2026
2e712f8
feat(rollout): firmware release channels tab + manage modal
Aug 4, 2026
585ebed
refactor(rollout): consolidate redundant labels, tighten button sizin…
Aug 4, 2026
c192222
feat(rollout): use compact button size in Apply-to tables
Aug 4, 2026
e476385
feat(rollout): rework firmware payload input for scale
Aug 5, 2026
b91849d
feat(rollout): apply design-review updates and contextual terminology
Aug 7, 2026
fce2b8c
refactor(rollout): scope Order to the efficiency axis, paced runs only
Aug 7, 2026
a460fd2
feat(rollout): adopt curtailment 'behavior' vocab for pacing section …
Aug 7, 2026
28f5a29
feat(rollout): release-channel create/manage toggle and responsive in…
Aug 10, 2026
7005bcf
feat(fleet): suggest rack creation from clustered unassigned miners
Aug 10, 2026
3afa949
feat(rollout): refine active rollout controls
jmarrxyz Aug 10, 2026
3ebb0f2
fix(rollout): refine scheduled update story UX
jmarrxyz Aug 10, 2026
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
@@ -1,21 +1,28 @@
import Button, { variants } from "@/shared/components/Button";
import Button, { sizes, variants } from "@/shared/components/Button";
import Row from "@/shared/components/Row";

interface TargetSelectButtonProps {
label: string;
value: string;
disabled?: boolean;
onClick: () => void;
/**
* Button size for the value control. Defaults to `base` to preserve the
* curtailment/schedule modals' existing sizing; the firmware rollout Apply-to
* tables opt into `compact`.
*/
size?: keyof typeof sizes;
}

function TargetSelectButton({ label, value, disabled = false, onClick }: TargetSelectButtonProps) {
function TargetSelectButton({ label, value, disabled = false, onClick, size = sizes.base }: TargetSelectButtonProps) {
return (
<Row compact className="flex items-center justify-between gap-4">
<span className="min-w-0 truncate text-emphasis-300 text-text-primary">{label}</span>
<Button
ariaLabel={`${label} ${value}`}
text={value}
variant={variants.secondary}
size={size}
disabled={disabled}
onClick={onClick}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { ComponentProps } from "react";
import type { Meta, StoryObj } from "@storybook/react";

import FleetContextualSuggestion from "./FleetContextualSuggestion";
import { Asic, Building, Fleet, Racks } from "@/shared/assets/icons";

const meta = {
title: "Proto Fleet/Fleet Management/FleetContextualSuggestion",
component: FleetContextualSuggestion,
parameters: {
layout: "padded",
},
} satisfies Meta<typeof FleetContextualSuggestion>;

export default meta;
type Story = StoryObj<typeof meta>;
type FleetContextualSuggestionProps = ComponentProps<typeof FleetContextualSuggestion>;

const noop = () => undefined;

const rackCreationArgs: FleetContextualSuggestionProps = {
icon: <Racks width="w-4" />,
title: "24 unassigned miners from 10.90.12.40-10.90.12.63 look like a rack.",
detail: "Seen in nearby IPs. Mostly Proto Rig.",
action: {
label: "Review",
onClick: noop,
},
onDismiss: noop,
};

const minerPairingArgs: FleetContextualSuggestionProps = {
icon: <Asic width="w-4" />,
title: "12 detected miners in 10.90.13.80-10.90.13.91 are ready to pair.",
detail: "Found during the latest network poll. All are reporting default credentials.",
action: {
label: "Review",
onClick: noop,
},
onDismiss: noop,
};

const containerCreationArgs: FleetContextualSuggestionProps = {
icon: <Fleet width="w-4" />,
title: "96 miners across 10.90.20.0/24 look like a container.",
detail: "Detected as four adjacent rack-sized cohorts with matching firmware and model.",
action: {
label: "Review",
onClick: noop,
},
onDismiss: noop,
};

const buildingGroupingArgs: FleetContextualSuggestionProps = {
icon: <Building width="w-4" />,
title: "4 rack-shaped cohorts in 10.90.0.0/20 look like Building B.",
detail: "Detected from configured IP ranges and contiguous rack groupings.",
action: {
label: "Review",
onClick: noop,
},
onDismiss: noop,
};

const examples = [rackCreationArgs, minerPairingArgs, containerCreationArgs, buildingGroupingArgs];

export const AllExamples: Story = {
args: rackCreationArgs,
render: () => (
<div className="flex max-w-5xl flex-col gap-3">
{examples.map((example) => (
<FleetContextualSuggestion key={example.title} {...example} />
))}
</div>
),
};

export const RackCreation: Story = {
args: rackCreationArgs,
};

export const MinerPairing: Story = {
args: minerPairingArgs,
};

export const ContainerCreation: Story = {
args: containerCreationArgs,
};

export const BuildingGrouping: Story = {
args: buildingGroupingArgs,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, test, vi } from "vitest";
import userEvent from "@testing-library/user-event";

import FleetContextualSuggestion from "./FleetContextualSuggestion";

describe("FleetContextualSuggestion", () => {
test("renders contextual copy and dispatches action", async () => {
const user = userEvent.setup();
const onReview = vi.fn();
const onDismiss = vi.fn();

render(
<FleetContextualSuggestion
title="24 unassigned miners from 10.90.12.40-10.90.12.63 look like a rack."
detail="Seen in nearby IPs. Mostly Proto Rig."
action={{ label: "Review", onClick: onReview }}
onDismiss={onDismiss}
/>,
);

expect(screen.getByText("24 unassigned miners from 10.90.12.40-10.90.12.63 look like a rack.")).toBeVisible();
expect(screen.getByText("Seen in nearby IPs. Mostly Proto Rig.")).toBeVisible();

await user.click(screen.getByRole("button", { name: "Review" }));
await user.click(screen.getByRole("button", { name: "Dismiss suggestion" }));

expect(onReview).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type ReactNode } from "react";
import clsx from "clsx";

import { DismissTiny, Info } from "@/shared/assets/icons";
import Button, { type ButtonVariant, sizes, variants } from "@/shared/components/Button";

export type FleetContextualSuggestionAction = {
label: string;
onClick: () => void;
variant?: ButtonVariant;
disabled?: boolean;
testId?: string;
};

type FleetContextualSuggestionProps = {
className?: string;
title: string;
detail?: string;
icon?: ReactNode;
action: FleetContextualSuggestionAction;
onDismiss?: () => void;
testId?: string;
};

const renderAction = (action: FleetContextualSuggestionAction) => (
<Button
key={action.label}
variant={action.variant ?? variants.primary}
size={sizes.compact}
className="min-h-8"
onClick={action.onClick}
disabled={action.disabled}
testId={action.testId}
>
{action.label}
</Button>
);

const FleetContextualSuggestion = ({
className,
title,
detail,
icon = <Info width="w-4" />,
action,
onDismiss,
testId = "fleet-contextual-suggestion",
}: FleetContextualSuggestionProps) => (
<div
className={clsx(
"flex flex-col gap-4 rounded-lg border border-border-5 bg-surface-elevated-base p-4 shadow-50 tablet:flex-row tablet:items-center tablet:justify-between",
className,
)}
data-testid={testId}
>
<div className="flex min-w-0 gap-3">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-core-primary-5 text-text-primary-70">
{icon}
</div>
<div className="min-w-0">
<div className="text-emphasis-300 text-text-primary">{title}</div>
{detail ? <div className="mt-1 text-300 text-text-primary-70">{detail}</div> : null}
</div>
</div>
<div className="flex shrink-0 flex-wrap items-center gap-2 pl-11 tablet:justify-end tablet:pl-0">
{renderAction(action)}
{onDismiss ? (
<Button
variant={variants.ghost}
size={sizes.compact}
ariaLabel="Dismiss suggestion"
className="min-h-8"
prefixIcon={<DismissTiny />}
onClick={onDismiss}
testId={`${testId}-dismiss`}
/>
) : null}
</div>
</div>
);

export default FleetContextualSuggestion;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fragment, type ReactNode, useCallback, useEffect, useState } from "react";
import clsx from "clsx";

import { Ellipsis } from "@/shared/assets/icons";
import { iconSizes } from "@/shared/assets/icons/constants";
Expand All @@ -19,6 +20,7 @@ export interface RowAction {
showGroupDivider?: boolean;
hidden?: boolean;
disabled?: boolean;
danger?: boolean;
testId?: string;
}

Expand Down Expand Up @@ -104,7 +106,7 @@ const RowActionsMenuInner = ({
setPopoverRenderMode("portal-fixed");
}, [setPopoverRenderMode]);

// Disabled hard-closes; re-enable doesn't resurrect operator must reopen.
// Disabled hard-closes; re-enable doesn't resurrect, operator must reopen.
const open = isOpen && !disabled;

const setMenuOpen = useCallback(
Expand Down Expand Up @@ -146,6 +148,7 @@ const RowActionsMenuInner = ({
<ActionSheet
items={visibleActions.map((action) => ({
disabled: action.disabled,
danger: action.danger,
icon: action.icon,
label: action.label,
onClick: action.onClick,
Expand Down Expand Up @@ -178,7 +181,7 @@ const RowActionsMenuInner = ({
<Fragment key={action.testId ?? `${action.label}-${index}`}>
<div className="px-4">
<Row
className="text-emphasis-300"
className={clsx("text-emphasis-300", action.danger && "text-intent-critical-fill")}
prefixIcon={action.icon}
testId={action.testId}
onClick={() => {
Expand Down
Loading