chore(site): refactor stories and tests from components directory (#9578)

* Refactor Alert

* Refactor Avatar and its stories

* Refactor AvatarData and its stories

* Refactor CodeExample and its tests

* Refactor ServiceBanner stories

* Refactor Navbar and its tests

* Refactor ServiceBanner stories

* Refactor LicenseBannerView stories

* Refactor DeploymentBannerView stories

* Extract optionValue into a module

* Refactor DeleteDialog stories

* Refactor ConfirmDialog tests

* Refactor EmptyState tests

* Flat ErrorBoundaryState and refactor stories

* Refactor Expander stories

* Refactor FormFooter stories

* Refactor FullPageForm stories

* Refactor EnterpriseSnackbar stories

* Refactor GroupAvatar stories

* Refactor HelpTooltip stories and remove index

* Remove unecessary types module from IconField

* Refactor LoadingButton stories

* Refactor Margins stories

* Refactor Markdown stories

* Refactor PageHeader stories

* Refactor PageButton tests

* Refactor Pill stories

* Refactor Resources stories

* Refactor RichParameterInput stories and flat MultiTextField

* Remove unecessary Stack story

* Refactor TableRowMenu stories

* Refactor TemplateLayout stories

* Refactor Typography props

* Refactor UserAutocomplete

* Refactor WorkspaceBuildLogs components and tests

* Refactor WorkspaceStatusBadge stories

* Fix wrong imports

* Remove Example.args pattern

* Fix wrong import

* Refactor EmptyState stories

* Refactor HelpTooltip stories

* Remove not valid ErrorAlert story

* Fix AvatarData story

* Add border back to CodeExample

* Fix Navbar story

* Fix AgentRow proxy in the stories
This commit is contained in:
Bruno Quaresma
2023-09-07 13:38:28 -03:00
committed by GitHub
parent 4f142fa959
commit 869d040cc6
85 changed files with 1382 additions and 1836 deletions

View File

@ -1,6 +1,5 @@
import { Alert } from "./Alert"; import { Alert } from "./Alert";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof Alert> = { const meta: Meta<typeof Alert> = {
@ -21,7 +20,6 @@ export const Success: Story = {
args: { args: {
children: "You're doing great!", children: "You're doing great!",
severity: "success", severity: "success",
onRetry: undefined,
}, },
}; };
@ -56,14 +54,3 @@ export const WarningWithActionAndDismiss: Story = {
severity: "warning", severity: "warning",
}, },
}; };
export const WithChildren: Story = {
args: {
severity: "warning",
children: (
<div>
This is a message with a <Link href="#">link</Link>
</div>
),
},
};

View File

@ -8,14 +8,12 @@ import Box from "@mui/material/Box";
export type AlertProps = MuiAlertProps & { export type AlertProps = MuiAlertProps & {
actions?: ReactNode; actions?: ReactNode;
dismissible?: boolean; dismissible?: boolean;
onRetry?: () => void;
onDismiss?: () => void; onDismiss?: () => void;
}; };
export const Alert: FC<AlertProps> = ({ export const Alert: FC<AlertProps> = ({
children, children,
actions, actions,
onRetry,
dismissible, dismissible,
severity, severity,
onDismiss, onDismiss,
@ -34,13 +32,6 @@ export const Alert: FC<AlertProps> = ({
{/* CTAs passed in by the consumer */} {/* CTAs passed in by the consumer */}
{actions} {actions}
{/* retry CTA */}
{onRetry && (
<Button variant="text" size="small" onClick={onRetry}>
Retry
</Button>
)}
{/* close CTA */} {/* close CTA */}
{dismissible && ( {dismissible && (
<Button <Button

View File

@ -1,7 +1,6 @@
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { mockApiError } from "testHelpers/entities"; import { mockApiError } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import { ErrorAlert } from "./ErrorAlert"; import { ErrorAlert } from "./ErrorAlert";
const mockError = mockApiError({ const mockError = mockApiError({
@ -15,7 +14,6 @@ const meta: Meta<typeof ErrorAlert> = {
args: { args: {
error: mockError, error: mockError,
dismissible: false, dismissible: false,
onRetry: undefined,
}, },
}; };
@ -55,21 +53,6 @@ export const WithActionAndDismiss: Story = {
}, },
}; };
export const WithRetry: Story = {
args: {
onRetry: action("retry"),
dismissible: true,
},
};
export const WithActionRetryAndDismiss: Story = {
args: {
actions: [ExampleAction],
onRetry: action("retry"),
dismissible: true,
},
};
export const WithNonApiError: Story = { export const WithNonApiError: Story = {
args: { args: {
error: new Error("Non API error here"), error: new Error("Non API error here"),

View File

@ -1,63 +1,71 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { Avatar, AvatarIcon, AvatarProps } from "./Avatar"; import { Avatar, AvatarIcon } from "./Avatar";
import PauseIcon from "@mui/icons-material/PauseOutlined"; import PauseIcon from "@mui/icons-material/PauseOutlined";
export default { const meta: Meta<typeof Avatar> = {
title: "components/Avatar", title: "components/Avatar",
component: Avatar, component: Avatar,
}; };
const Template: Story<AvatarProps> = (args: AvatarProps) => ( export default meta;
<Avatar {...args} /> type Story = StoryObj<typeof Avatar>;
);
export const Letter = Template.bind({}); export const Letter: Story = {
Letter.args = { args: {
children: "Coder", children: "Coder",
},
}; };
export const LetterXL = Template.bind({}); export const LetterXL = {
LetterXL.args = { args: {
children: "Coder", children: "Coder",
size: "xl", size: "xl",
},
}; };
export const LetterDarken = Template.bind({}); export const LetterDarken = {
LetterDarken.args = { args: {
children: "Coder", children: "Coder",
colorScheme: "darken", colorScheme: "darken",
},
}; };
export const Image = Template.bind({}); export const Image = {
Image.args = { args: {
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
},
}; };
export const ImageXL = Template.bind({}); export const ImageXL = {
ImageXL.args = { args: {
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
size: "xl", size: "xl",
},
}; };
export const MuiIcon = Template.bind({}); export const MuiIcon = {
MuiIcon.args = { args: {
children: <PauseIcon />, children: <PauseIcon />,
},
}; };
export const MuiIconDarken = Template.bind({}); export const MuiIconDarken = {
MuiIconDarken.args = { args: {
children: <PauseIcon />, children: <PauseIcon />,
colorScheme: "darken", colorScheme: "darken",
},
}; };
export const MuiIconXL = Template.bind({}); export const MuiIconXL = {
MuiIconXL.args = { args: {
children: <PauseIcon />, children: <PauseIcon />,
size: "xl", size: "xl",
},
}; };
export const AvatarIconDarken = Template.bind({}); export const AvatarIconDarken = {
AvatarIconDarken.args = { args: {
children: <AvatarIcon src="/icon/database.svg" />, children: <AvatarIcon src="/icon/database.svg" />,
colorScheme: "darken", colorScheme: "darken",
},
}; };

View File

@ -4,7 +4,6 @@ import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { FC } from "react"; import { FC } from "react";
import { combineClasses } from "utils/combineClasses"; import { combineClasses } from "utils/combineClasses";
import { firstLetter } from "./firstLetter";
export type AvatarProps = MuiAvatarProps & { export type AvatarProps = MuiAvatarProps & {
size?: "sm" | "md" | "xl"; size?: "sm" | "md" | "xl";
@ -32,7 +31,6 @@ export const Avatar: FC<AvatarProps> = ({
fitImage && styles.fitImage, fitImage && styles.fitImage,
])} ])}
> >
{/* If the children is a string, we always want to render the first letter */}
{typeof children === "string" ? firstLetter(children) : children} {typeof children === "string" ? firstLetter(children) : children}
</MuiAvatar> </MuiAvatar>
); );
@ -46,6 +44,14 @@ export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
return <img src={src} alt="" className={styles.avatarIcon} />; return <img src={src} alt="" className={styles.avatarIcon} />;
}; };
const firstLetter = (str: string): string => {
if (str.length > 0) {
return str[0].toLocaleUpperCase();
}
return "";
};
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
// Size styles // Size styles
sm: { sm: {

View File

@ -1,11 +0,0 @@
import { firstLetter } from "./firstLetter";
describe("first-letter", () => {
it.each<[string, string]>([
["", ""],
["User", "U"],
["test", "T"],
])(`firstLetter(%p) returns %p`, (input, expected) => {
expect(firstLetter(input)).toBe(expected);
});
});

View File

@ -1,10 +0,0 @@
/**
* firstLetter extracts the first character and returns it, uppercased.
*/
export const firstLetter = (str: string): string => {
if (str.length > 0) {
return str[0].toLocaleUpperCase();
}
return "";
};

View File

@ -1,24 +1,22 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { AvatarData, AvatarDataProps } from "./AvatarData"; import { AvatarData } from "./AvatarData";
export default { const meta: Meta<typeof AvatarData> = {
title: "components/AvatarData", title: "components/AvatarData",
component: AvatarData, component: AvatarData,
args: {
title: "coder",
subtitle: "coder@coder.com",
},
}; };
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => ( export default meta;
<AvatarData {...args} /> type Story = StoryObj<typeof AvatarData>;
);
export const Example = Template.bind({}); export const WithTitleAndSubtitle: Story = {};
Example.args = {
title: "coder",
subtitle: "coder@coder.com",
};
export const WithImage = Template.bind({}); export const WithImage: Story = {
WithImage.args = { args: {
title: "coder", src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
subtitle: "coder@coder.com", },
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
}; };

View File

@ -1,5 +1,5 @@
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import { FC, PropsWithChildren } from "react"; import { FC } from "react";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
@ -10,7 +10,7 @@ export interface AvatarDataProps {
avatar?: React.ReactNode; avatar?: React.ReactNode;
} }
export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({ export const AvatarData: FC<AvatarDataProps> = ({
title, title,
subtitle, subtitle,
src, src,

View File

@ -1,9 +1,9 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { CodeExample, CodeExampleProps } from "./CodeExample"; import { CodeExample } from "./CodeExample";
const sampleCode = `echo "Hello, world"`; const sampleCode = `echo "Hello, world"`;
export default { const meta: Meta<typeof CodeExample> = {
title: "components/CodeExample", title: "components/CodeExample",
component: CodeExample, component: CodeExample,
argTypes: { argTypes: {
@ -11,16 +11,17 @@ export default {
}, },
}; };
const Template: Story<CodeExampleProps> = (args: CodeExampleProps) => ( export default meta;
<CodeExample {...args} /> type Story = StoryObj<typeof CodeExample>;
);
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
code: sampleCode, code: sampleCode,
},
}; };
export const LongCode = Template.bind({}); export const LongCode: Story = {
LongCode.args = { args: {
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L", code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
},
}; };

View File

@ -1,14 +0,0 @@
import { screen } from "@testing-library/react";
import { render } from "../../testHelpers/renderHelpers";
import { CodeExample } from "./CodeExample";
describe("CodeExample", () => {
it("renders code", async () => {
// When
render(<CodeExample code="echo hello" />);
// Then
// Both lines should be rendered
await screen.findByText("echo hello");
});
});

View File

@ -7,33 +7,24 @@ import { Theme } from "@mui/material/styles";
export interface CodeExampleProps { export interface CodeExampleProps {
code: string; code: string;
className?: string;
buttonClassName?: string;
tooltipTitle?: string;
inline?: boolean;
password?: boolean; password?: boolean;
className?: string;
} }
/** /**
* Component to show single-line code examples, with a copy button * Component to show single-line code examples, with a copy button
*/ */
export const CodeExample: FC<React.PropsWithChildren<CodeExampleProps>> = ({ export const CodeExample: FC<CodeExampleProps> = ({
code, code,
password,
className, className,
buttonClassName,
tooltipTitle,
inline,
}) => { }) => {
const styles = useStyles({ inline: inline }); const styles = useStyles({ password });
return ( return (
<div className={combineClasses([styles.root, className])}> <div className={combineClasses([styles.root, className])}>
<code className={styles.code}>{code}</code> <code className={styles.code}>{code}</code>
<CopyButton <CopyButton text={code} />
text={code}
tooltipTitle={tooltipTitle}
buttonClassName={buttonClassName}
/>
</div> </div>
); );
}; };
@ -48,14 +39,14 @@ const useStyles = makeStyles<Theme, styleProps>((theme) => ({
display: props.inline ? "inline-flex" : "flex", display: props.inline ? "inline-flex" : "flex",
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
background: props.inline ? "rgb(0 0 0 / 30%)" : "hsl(223, 27%, 3%)", background: "rgb(0 0 0 / 30%)",
border: props.inline ? undefined : `1px solid ${theme.palette.divider}`,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
fontFamily: MONOSPACE_FONT_FAMILY, fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14, fontSize: 14,
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
padding: theme.spacing(1), padding: theme.spacing(1),
lineHeight: "150%", lineHeight: "150%",
border: `1px solid ${theme.palette.divider}`,
}), }),
code: { code: {
padding: theme.spacing(0, 1), padding: theme.spacing(0, 1),

View File

@ -1,20 +1,16 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { MockDeploymentStats } from "testHelpers/entities"; import { MockDeploymentStats } from "testHelpers/entities";
import { import { DeploymentBannerView } from "./DeploymentBannerView";
DeploymentBannerView,
DeploymentBannerViewProps,
} from "./DeploymentBannerView";
export default { const meta: Meta<typeof DeploymentBannerView> = {
title: "components/DeploymentBannerView", title: "components/DeploymentBannerView",
component: DeploymentBannerView, component: DeploymentBannerView,
args: {
stats: MockDeploymentStats,
},
}; };
const Template: Story<DeploymentBannerViewProps> = (args) => ( export default meta;
<DeploymentBannerView {...args} /> type Story = StoryObj<typeof DeploymentBannerView>;
);
export const Preview = Template.bind({}); export const Preview: Story = {};
Preview.args = {
stats: MockDeploymentStats,
};

View File

@ -1,34 +1,36 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView"; import { LicenseBannerView } from "./LicenseBannerView";
export default { const meta: Meta<typeof LicenseBannerView> = {
title: "components/LicenseBannerView", title: "components/LicenseBannerView",
component: LicenseBannerView, component: LicenseBannerView,
}; };
const Template: Story<LicenseBannerViewProps> = (args) => ( export default meta;
<LicenseBannerView {...args} /> type Story = StoryObj<typeof LicenseBannerView>;
);
export const OneWarning = Template.bind({}); export const OneWarning: Story = {
OneWarning.args = { args: {
errors: [], errors: [],
warnings: ["You have exceeded the number of seats in your license."], warnings: ["You have exceeded the number of seats in your license."],
},
}; };
export const TwoWarnings = Template.bind({}); export const TwoWarnings: Story = {
TwoWarnings.args = { args: {
errors: [], errors: [],
warnings: [ warnings: [
"You have exceeded the number of seats in your license.", "You have exceeded the number of seats in your license.",
"You are flying too close to the sun.", "You are flying too close to the sun.",
], ],
},
}; };
export const OneError = Template.bind({}); export const OneError: Story = {
OneError.args = { args: {
errors: [ errors: [
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.", "You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.",
], ],
warnings: [], warnings: [],
},
}; };

View File

@ -1,45 +1,34 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { MockUser, MockUser2 } from "../../../testHelpers/entities"; import { MockUser, MockUser2 } from "../../../testHelpers/entities";
import { NavbarView, NavbarViewProps } from "./NavbarView"; import { NavbarView } from "./NavbarView";
export default { const meta: Meta<typeof NavbarView> = {
title: "components/NavbarView", title: "components/NavbarView",
component: NavbarView, component: NavbarView,
argTypes: { args: {
onSignOut: { action: "Sign Out" }, user: MockUser,
}, },
}; };
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => ( export default meta;
<NavbarView {...args} /> type Story = StoryObj<typeof NavbarView>;
);
export const ForAdmin = Template.bind({}); export const ForAdmin: Story = {};
ForAdmin.args = {
user: MockUser, export const ForMember: Story = {
onSignOut: () => { args: {
return Promise.resolve(); user: MockUser2,
canViewAuditLog: false,
canViewDeployment: false,
canViewAllUsers: false,
}, },
}; };
export const ForMember = Template.bind({}); export const SmallViewport: Story = {
ForMember.args = { parameters: {
user: MockUser2, viewport: {
onSignOut: () => { defaultViewport: "tablet",
return Promise.resolve(); },
chromatic: { viewports: [420] },
}, },
}; };
export const SmallViewport = Template.bind({});
SmallViewport.args = {
user: MockUser,
onSignOut: () => {
return Promise.resolve();
},
};
SmallViewport.parameters = {
viewport: {
defaultViewport: "tablet",
},
chromatic: { viewports: [420] },
};

View File

@ -2,7 +2,6 @@ import { screen } from "@testing-library/react";
import { import {
MockPrimaryWorkspaceProxy, MockPrimaryWorkspaceProxy,
MockUser, MockUser,
MockUser2,
} from "../../../testHelpers/entities"; } from "../../../testHelpers/entities";
import { renderWithAuth } from "../../../testHelpers/renderHelpers"; import { renderWithAuth } from "../../../testHelpers/renderHelpers";
import { Language as navLanguage, NavbarView } from "./NavbarView"; import { Language as navLanguage, NavbarView } from "./NavbarView";
@ -28,18 +27,6 @@ describe("NavbarView", () => {
return; return;
}; };
const env = process.env;
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
beforeEach(() => {
process.env = { ...env };
});
// REMARK: restoring process.env
afterEach(() => {
process.env = env;
});
it("workspaces nav link has the correct href", async () => { it("workspaces nav link has the correct href", async () => {
renderWithAuth( renderWithAuth(
<NavbarView <NavbarView
@ -85,32 +72,6 @@ describe("NavbarView", () => {
expect((userLink as HTMLAnchorElement).href).toContain("/users"); expect((userLink as HTMLAnchorElement).href).toContain("/users");
}); });
it("renders profile picture for user", async () => {
// Given
const mockUser = {
...MockUser,
username: "bryan",
avatar_url: "",
};
// When
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={mockUser}
onSignOut={noop}
canViewAuditLog
canViewDeployment
canViewAllUsers
/>,
);
// Then
// There should be a 'B' avatar!
const element = await screen.findByText("B");
expect(element).toBeDefined();
});
it("audit nav link has the correct href", async () => { it("audit nav link has the correct href", async () => {
renderWithAuth( renderWithAuth(
<NavbarView <NavbarView
@ -126,21 +87,6 @@ describe("NavbarView", () => {
expect((auditLink as HTMLAnchorElement).href).toContain("/audit"); expect((auditLink as HTMLAnchorElement).href).toContain("/audit");
}); });
it("audit nav link is hidden for members", async () => {
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser2}
onSignOut={noop}
canViewAuditLog={false}
canViewDeployment
canViewAllUsers
/>,
);
const auditLink = screen.queryByText(navLanguage.audit);
expect(auditLink).not.toBeInTheDocument();
});
it("deployment nav link has the correct href", async () => { it("deployment nav link has the correct href", async () => {
renderWithAuth( renderWithAuth(
<NavbarView <NavbarView
@ -157,19 +103,4 @@ describe("NavbarView", () => {
"/deployment/general", "/deployment/general",
); );
}); });
it("deployment nav link is hidden for members", async () => {
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser2}
onSignOut={noop}
canViewAuditLog={false}
canViewDeployment={false}
canViewAllUsers
/>,
);
const auditLink = screen.queryByText(navLanguage.deployment);
expect(auditLink).not.toBeInTheDocument();
});
}); });

View File

@ -3,8 +3,8 @@ import { makeStyles } from "@mui/styles";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import { FC } from "react"; import { FC } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { ellipsizeText } from "../../../../../utils/ellipsizeText"; import { ellipsizeText } from "utils/ellipsizeText";
import { Typography } from "../../../../Typography/Typography"; import { Typography } from "components/Typography/Typography";
type BorderedMenuRowVariant = "narrow" | "wide"; type BorderedMenuRowVariant = "narrow" | "wide";

View File

@ -1,26 +1,16 @@
import Box from "@mui/material/Box";
import { Story } from "@storybook/react";
import { MockUser } from "../../../../testHelpers/entities"; import { MockUser } from "../../../../testHelpers/entities";
import { UserDropdown, UserDropdownProps } from "./UserDropdown"; import { UserDropdown } from "./UserDropdown";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof UserDropdown> = {
title: "components/UserDropdown", title: "components/UserDropdown",
component: UserDropdown, component: UserDropdown,
argTypes: { args: {
onSignOut: { action: "Sign Out" }, user: MockUser,
}, },
}; };
const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => ( export default meta;
<Box style={{ backgroundColor: "#000", width: 88 }}> type Story = StoryObj<typeof UserDropdown>;
<UserDropdown {...args} />
</Box>
);
export const Example = Template.bind({}); export const Example: Story = {};
Example.args = {
user: MockUser,
onSignOut: () => {
return Promise.resolve();
},
};

View File

@ -1,30 +0,0 @@
import { fireEvent, screen } from "@testing-library/react";
import { MockSupportLinks, MockUser } from "../../../../testHelpers/entities";
import { render } from "../../../../testHelpers/renderHelpers";
import { Language } from "./UserDropdownContent/UserDropdownContent";
import { UserDropdown, UserDropdownProps } from "./UserDropdown";
const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
render(
<UserDropdown
user={props.user ?? MockUser}
supportLinks={MockSupportLinks}
onSignOut={props.onSignOut ?? jest.fn()}
/>,
);
const trigger = await screen.findByTestId("user-dropdown-trigger");
fireEvent.click(trigger);
};
describe("UserDropdown", () => {
describe("when the trigger is clicked", () => {
it("opens the menu", async () => {
await renderAndClick();
expect(screen.getByText(Language.accountLabel)).toBeDefined();
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined();
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined();
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined();
expect(screen.getByText(Language.signOutLabel)).toBeDefined();
});
});
});

View File

@ -3,15 +3,15 @@ import MenuItem from "@mui/material/MenuItem";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { useState, FC, PropsWithChildren, MouseEvent } from "react"; import { useState, FC, PropsWithChildren, MouseEvent } from "react";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
import * as TypesGen from "../../../../api/typesGenerated"; import * as TypesGen from "api/typesGenerated";
import { navHeight } from "../../../../theme/constants"; import { navHeight } from "theme/constants";
import { BorderedMenu } from "./BorderedMenu/BorderedMenu"; import { BorderedMenu } from "./BorderedMenu";
import { import {
CloseDropdown, CloseDropdown,
OpenDropdown, OpenDropdown,
} from "../../../DropdownArrows/DropdownArrows"; } from "components/DropdownArrows/DropdownArrows";
import { UserAvatar } from "../../../UserAvatar/UserAvatar"; import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent"; import { UserDropdownContent } from "./UserDropdownContent";
import { BUTTON_SM_HEIGHT } from "theme/theme"; import { BUTTON_SM_HEIGHT } from "theme/theme";
export interface UserDropdownProps { export interface UserDropdownProps {

View File

@ -0,0 +1,42 @@
import { MockUser } from "testHelpers/entities";
import { UserDropdownContent } from "./UserDropdownContent";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof UserDropdownContent> = {
title: "components/UserDropdownContent",
component: UserDropdownContent,
};
export default meta;
type Story = StoryObj<typeof UserDropdownContent>;
export const ExampleNoRoles: Story = {
args: {
user: {
...MockUser,
roles: [],
},
},
};
export const ExampleOneRole: Story = {
args: {
user: {
...MockUser,
roles: [{ name: "member", display_name: "Member" }],
},
},
};
export const ExampleThreeRoles: Story = {
args: {
user: {
...MockUser,
roles: [
{ name: "admin", display_name: "Admin" },
{ name: "member", display_name: "Member" },
{ name: "auditor", display_name: "Auditor" },
],
},
},
};

View File

@ -0,0 +1,36 @@
import { screen } from "@testing-library/react";
import { MockUser } from "testHelpers/entities";
import { render } from "testHelpers/renderHelpers";
import { Language, UserDropdownContent } from "./UserDropdownContent";
describe("UserDropdownContent", () => {
it("has the correct link for the account item", () => {
render(
<UserDropdownContent
user={MockUser}
onSignOut={jest.fn()}
onPopoverClose={jest.fn()}
/>,
);
const link = screen.getByText(Language.accountLabel).closest("a");
if (!link) {
throw new Error("Anchor tag not found for the account menu item");
}
expect(link.getAttribute("href")).toBe("/settings/account");
});
it("calls the onSignOut function", () => {
const onSignOut = jest.fn();
render(
<UserDropdownContent
user={MockUser}
onSignOut={onSignOut}
onPopoverClose={jest.fn()}
/>,
);
screen.getByText(Language.signOutLabel).click();
expect(onSignOut).toBeCalledTimes(1);
});
});

View File

@ -8,7 +8,7 @@ import LaunchIcon from "@mui/icons-material/LaunchOutlined";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { FC } from "react"; import { FC } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as TypesGen from "../../../../../api/typesGenerated"; import * as TypesGen from "api/typesGenerated";
import DocsIcon from "@mui/icons-material/MenuBook"; import DocsIcon from "@mui/icons-material/MenuBook";
import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"; import LogoutIcon from "@mui/icons-material/ExitToAppOutlined";
import { combineClasses } from "utils/combineClasses"; import { combineClasses } from "utils/combineClasses";

View File

@ -1,43 +0,0 @@
import { Story } from "@storybook/react";
import { MockUser } from "../../../../../testHelpers/entities";
import {
UserDropdownContent,
UserDropdownContentProps,
} from "./UserDropdownContent";
export default {
title: "components/UserDropdownContent",
component: UserDropdownContent,
};
const Template: Story<UserDropdownContentProps> = (args) => (
<UserDropdownContent {...args} />
);
export const ExampleNoRoles = Template.bind({});
ExampleNoRoles.args = {
user: {
...MockUser,
roles: [],
},
};
export const ExampleOneRole = Template.bind({});
ExampleOneRole.args = {
user: {
...MockUser,
roles: [{ name: "member", display_name: "Member" }],
},
};
export const ExampleThreeRoles = Template.bind({});
ExampleThreeRoles.args = {
user: {
...MockUser,
roles: [
{ name: "admin", display_name: "Admin" },
{ name: "member", display_name: "Member" },
{ name: "auditor", display_name: "Auditor" },
],
},
};

View File

@ -1,77 +0,0 @@
import { screen } from "@testing-library/react";
import {
MockBuildInfo,
MockSupportLinks,
MockUser,
} from "../../../../../testHelpers/entities";
import { render } from "../../../../../testHelpers/renderHelpers";
import { Language, UserDropdownContent } from "./UserDropdownContent";
describe("UserDropdownContent", () => {
const env = process.env;
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
beforeEach(() => {
process.env = { ...env };
});
// REMARK: restoring process.env
afterEach(() => {
process.env = env;
});
it("displays the menu items", () => {
render(
<UserDropdownContent
user={MockUser}
buildInfo={MockBuildInfo}
supportLinks={MockSupportLinks}
onSignOut={jest.fn()}
onPopoverClose={jest.fn()}
/>,
);
expect(screen.getByText(Language.accountLabel)).toBeDefined();
expect(screen.getByText(Language.signOutLabel)).toBeDefined();
expect(screen.getByText(Language.copyrightText)).toBeDefined();
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined();
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined();
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined();
expect(
screen.getByText(MockSupportLinks[2].name).closest("a"),
).toHaveAttribute(
"href",
"https://github.com/coder/coder/issues/new?labels=needs+grooming&body=Version%3A%20%5B%60v99.999.9999%2Bc9cdf14%60%5D(file%3A%2F%2F%2Fmock-url)",
);
expect(screen.getByText(MockBuildInfo.version)).toBeDefined();
});
it("has the correct link for the account item", () => {
render(
<UserDropdownContent
user={MockUser}
onSignOut={jest.fn()}
onPopoverClose={jest.fn()}
/>,
);
const link = screen.getByText(Language.accountLabel).closest("a");
if (!link) {
throw new Error("Anchor tag not found for the account menu item");
}
expect(link.getAttribute("href")).toBe("/settings/account");
});
it("calls the onSignOut function", () => {
const onSignOut = jest.fn();
render(
<UserDropdownContent
user={MockUser}
onSignOut={onSignOut}
onPopoverClose={jest.fn()}
/>,
);
screen.getByText(Language.signOutLabel).click();
expect(onSignOut).toBeCalledTimes(1);
});
});

View File

@ -1,24 +1,25 @@
import { Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView"; import { ServiceBannerView } from "./ServiceBannerView";
export default { const meta: Meta<typeof ServiceBannerView> = {
title: "components/ServiceBannerView", title: "components/ServiceBannerView",
component: ServiceBannerView, component: ServiceBannerView,
}; };
const Template: Story<ServiceBannerViewProps> = (args) => ( export default meta;
<ServiceBannerView {...args} /> type Story = StoryObj<typeof ServiceBannerView>;
);
export const Production = Template.bind({}); export const Production: Story = {
Production.args = { args: {
message: "weeeee", message: "weeeee",
backgroundColor: "#FFFFFF", backgroundColor: "#FFFFFF",
},
}; };
export const Preview = Template.bind({}); export const Preview: Story = {
Preview.args = { args: {
message: "weeeee", message: "weeeee",
backgroundColor: "#000000", backgroundColor: "#000000",
preview: true, preview: true,
},
}; };

View File

@ -12,7 +12,7 @@ import {
OptionValue, OptionValue,
} from "components/DeploySettingsLayout/Option"; } from "components/DeploySettingsLayout/Option";
import { FC } from "react"; import { FC } from "react";
import { intervalToDuration, formatDuration } from "date-fns"; import { optionValue } from "./optionValue";
const OptionsTable: FC<{ const OptionsTable: FC<{
options: DeploymentOption[]; options: DeploymentOption[];
@ -60,29 +60,6 @@ const OptionsTable: FC<{
); );
}; };
// optionValue is a helper function to format the value of a specific deployment options
export function optionValue(option: DeploymentOption) {
switch (option.name) {
case "Max Token Lifetime":
case "Session Duration":
// intervalToDuration takes ms, so convert nanoseconds to ms
return formatDuration(
intervalToDuration({ start: 0, end: (option.value as number) / 1e6 }),
);
case "Strict-Transport-Security":
if (option.value === 0) {
return "Disabled";
}
return (option.value as number).toString() + "s";
case "OIDC Group Mapping":
return Object.entries(option.value as Record<string, string>).map(
([key, value]) => `"${key}"->"${value}"`,
);
default:
return option.value;
}
}
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
"& td": { "& td": {

View File

@ -1,4 +1,4 @@
import { optionValue } from "./OptionsTable"; import { optionValue } from "./optionValue";
import { DeploymentOption } from "api/types"; import { DeploymentOption } from "api/types";
const defaultOption: DeploymentOption = { const defaultOption: DeploymentOption = {

View File

@ -0,0 +1,25 @@
import { DeploymentOption } from "api/types";
import { intervalToDuration, formatDuration } from "date-fns";
// optionValue is a helper function to format the value of a specific deployment options
export function optionValue(option: DeploymentOption) {
switch (option.name) {
case "Max Token Lifetime":
case "Session Duration":
// intervalToDuration takes ms, so convert nanoseconds to ms
return formatDuration(
intervalToDuration({ start: 0, end: (option.value as number) / 1e6 }),
);
case "Strict-Transport-Security":
if (option.value === 0) {
return "Disabled";
}
return (option.value as number).toString() + "s";
case "OIDC Group Mapping":
return Object.entries(option.value as Record<string, string>).map(
([key, value]) => `"${key}"->"${value}"`,
);
default:
return option.value;
}
}

View File

@ -1,58 +1,57 @@
import { action } from "@storybook/addon-actions"; import { action } from "@storybook/addon-actions";
import { ComponentMeta, Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog"; import { ConfirmDialog } from "./ConfirmDialog";
export default { const meta: Meta<typeof ConfirmDialog> = {
title: "Components/Dialogs/ConfirmDialog", title: "components/Dialogs/ConfirmDialog",
component: ConfirmDialog, component: ConfirmDialog,
argTypes: {
open: {
control: "boolean",
},
},
args: { args: {
onClose: action("onClose"), onClose: action("onClose"),
onConfirm: action("onConfirm"), onConfirm: action("onConfirm"),
open: true, open: true,
title: "Confirm Dialog", title: "Confirm Dialog",
}, },
} as ComponentMeta<typeof ConfirmDialog>;
const Template: Story<ConfirmDialogProps> = (args) => (
<ConfirmDialog {...args} />
);
export const DeleteDialog = Template.bind({});
DeleteDialog.args = {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
}; };
export const InfoDialog = Template.bind({}); export default meta;
InfoDialog.args = { type Story = StoryObj<typeof ConfirmDialog>;
description: "Information is cool!",
hideCancel: true, export const Example: Story = {
type: "info", args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
},
}; };
export const InfoDialogWithCancel = Template.bind({}); export const InfoDialog: Story = {
InfoDialogWithCancel.args = { args: {
description: "Information can be cool!", description: "Information is cool!",
hideCancel: false, hideCancel: true,
type: "info", type: "info",
},
}; };
export const SuccessDialog = Template.bind({}); export const InfoDialogWithCancel: Story = {
SuccessDialog.args = { args: {
description: "I am successful.", description: "Information can be cool!",
hideCancel: true, hideCancel: false,
type: "success", type: "info",
},
}; };
export const SuccessDialogWithCancel = Template.bind({}); export const SuccessDialog: Story = {
SuccessDialogWithCancel.args = { args: {
description: "I may be successful.", description: "I am successful.",
hideCancel: false, hideCancel: true,
type: "success", type: "success",
},
};
export const SuccessDialogWithCancel: Story = {
args: {
description: "I may be successful.",
hideCancel: false,
type: "success",
},
}; };

View File

@ -1,96 +1,8 @@
import { fireEvent, screen } from "@testing-library/react"; import { fireEvent, screen } from "@testing-library/react";
import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog"; import { ConfirmDialog } from "./ConfirmDialog";
import { render } from "testHelpers/renderHelpers"; import { render } from "testHelpers/renderHelpers";
describe("ConfirmDialog", () => { describe("ConfirmDialog", () => {
it("renders", () => {
// Given
const onCloseMock = jest.fn();
const props = {
onClose: onCloseMock,
open: true,
title: "Test",
};
// When
render(<ConfirmDialog {...props} />);
// Then
expect(screen.getByRole("dialog")).toBeDefined();
});
it("does not display cancel for info dialogs", () => {
// Given (note that info is the default)
const onCloseMock = jest.fn();
const props = {
cancelText: "CANCEL",
onClose: onCloseMock,
open: true,
title: "Test",
};
// When
render(<ConfirmDialog {...props} />);
// Then
expect(screen.queryByText("CANCEL")).toBeNull();
});
it("can display cancel when normally hidden", () => {
// Given
const onCloseMock = jest.fn();
const props = {
cancelText: "CANCEL",
onClose: onCloseMock,
open: true,
title: "Test",
hideCancel: false,
};
// When
render(<ConfirmDialog {...props} />);
// Then
expect(screen.getByText("CANCEL")).toBeDefined();
});
it("displays cancel for delete dialogs", () => {
// Given
const onCloseMock = jest.fn();
const props: ConfirmDialogProps = {
cancelText: "CANCEL",
onClose: onCloseMock,
open: true,
title: "Test",
type: "delete",
};
// When
render(<ConfirmDialog {...props} />);
// Then
expect(screen.getByText("CANCEL")).toBeDefined();
});
it("can hide cancel when normally visible", () => {
// Given
const onCloseMock = jest.fn();
const props: ConfirmDialogProps = {
cancelText: "CANCEL",
onClose: onCloseMock,
open: true,
title: "Test",
hideCancel: true,
type: "delete",
};
// When
render(<ConfirmDialog {...props} />);
// Then
expect(screen.queryByText("CANCEL")).toBeNull();
});
it("onClose is called when cancelled", () => { it("onClose is called when cancelled", () => {
// Given // Given
const onCloseMock = jest.fn(); const onCloseMock = jest.fn();

View File

@ -1,28 +1,21 @@
import { action } from "@storybook/addon-actions"; import { action } from "@storybook/addon-actions";
import { ComponentMeta, Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { DeleteDialog, DeleteDialogProps } from "./DeleteDialog"; import { DeleteDialog } from "./DeleteDialog";
export default { const meta: Meta<typeof DeleteDialog> = {
title: "Components/Dialogs/DeleteDialog", title: "components/Dialogs/DeleteDialog",
component: DeleteDialog, component: DeleteDialog,
argTypes: {
open: {
control: "boolean",
},
},
args: { args: {
onCancel: action("onClose"), onCancel: action("onClose"),
onConfirm: action("onConfirm"), onConfirm: action("onConfirm"),
open: true, isOpen: true,
entity: "foo", entity: "foo",
name: "MyFoo", name: "MyFoo",
info: "Here's some info about the foo so you know you're deleting the right one.", info: "Here's some info about the foo so you know you're deleting the right one.",
}, },
} as ComponentMeta<typeof DeleteDialog>;
const Template: Story<DeleteDialogProps> = (args) => <DeleteDialog {...args} />;
export const Example = Template.bind({});
Example.args = {
isOpen: true,
}; };
export default meta;
type Story = StoryObj<typeof DeleteDialog>;
export const Example: Story = {};

View File

@ -0,0 +1,21 @@
import Button from "@mui/material/Button";
import { EmptyState } from "./EmptyState";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof EmptyState> = {
title: "components/EmptyState",
component: EmptyState,
args: {
message: "Create your first workspace",
},
};
export default meta;
type Story = StoryObj<typeof EmptyState>;
export const Example: Story = {
args: {
description: "It is easy, just click the button below",
cta: <Button>Create workspace</Button>,
},
};

View File

@ -1,36 +0,0 @@
import { screen } from "@testing-library/react";
import { render } from "../../testHelpers/renderHelpers";
import { EmptyState } from "./EmptyState";
describe("EmptyState", () => {
it("renders (smoke test)", async () => {
// When
render(<EmptyState message="Hello, world" />);
// Then
await screen.findByText("Hello, world");
});
it("renders description text", async () => {
// When
render(
<EmptyState message="Hello, world" description="Friendly greeting" />,
);
// Then
await screen.findByText("Hello, world");
await screen.findByText("Friendly greeting");
});
it("renders cta component", async () => {
// Given
const cta = <button title="Click me" />;
// When
render(<EmptyState message="Hello, world" cta={cta} />);
// Then
await screen.findByText("Hello, world");
await screen.findByRole("button");
});
});

View File

@ -1,5 +1,5 @@
import { Component, ReactNode, PropsWithChildren } from "react"; import { Component, ReactNode, PropsWithChildren } from "react";
import { RuntimeErrorState } from "./RuntimeErrorState/RuntimeErrorState"; import { RuntimeErrorState } from "./RuntimeErrorState";
type ErrorBoundaryProps = PropsWithChildren<unknown>; type ErrorBoundaryProps = PropsWithChildren<unknown>;

View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react";
import { RuntimeErrorState } from "./RuntimeErrorState";
const error = new Error("An error occurred");
const meta: Meta<typeof RuntimeErrorState> = {
title: "components/RuntimeErrorState",
component: RuntimeErrorState,
args: {
error,
},
parameters: {
// The RuntimeErrorState is noisy for chromatic, because it renders an actual error
// along with the stacktrace - and the stacktrace includes the full URL of
// scripts in the stack. This is problematic, because every deployment uses
// a different URL, causing the validation to fail.
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof RuntimeErrorState>;
export const Errored: Story = {};

View File

@ -9,7 +9,7 @@ import { FullScreenLoader } from "components/Loader/FullScreenLoader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { Margins } from "../../Margins/Margins"; import { Margins } from "components/Margins/Margins";
const fetchDynamicallyImportedModuleError = const fetchDynamicallyImportedModuleError =
"Failed to fetch dynamically imported module"; "Failed to fetch dynamically imported module";

View File

@ -1,29 +0,0 @@
import { Story } from "@storybook/react";
import { RuntimeErrorState, RuntimeErrorStateProps } from "./RuntimeErrorState";
const error = new Error("An error occurred");
export default {
title: "components/RuntimeErrorState",
component: RuntimeErrorState,
args: {
error,
},
};
const Template: Story<RuntimeErrorStateProps> = (args) => (
<RuntimeErrorState {...args} />
);
export const Errored = Template.bind({});
Errored.parameters = {
// The RuntimeErrorState is noisy for chromatic, because it renders an actual error
// along with the stacktrace - and the stacktrace includes the full URL of
// scripts in the stack. This is problematic, because every deployment uses
// a different URL, causing the validation to fail.
chromatic: { disableSnapshot: true },
};
Errored.args = {
error,
};

View File

@ -1,22 +1,22 @@
import { Story } from "@storybook/react"; import { Expander } from "./Expander";
import { Expander, ExpanderProps } from "./Expander"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof Expander> = {
title: "components/Expander", title: "components/Expander",
component: Expander, component: Expander,
argTypes: { };
setExpanded: { action: "setExpanded" },
export default meta;
type Story = StoryObj<typeof Expander>;
export const Expanded: Story = {
args: {
expanded: true,
}, },
}; };
const Template: Story<ExpanderProps> = (args) => <Expander {...args} />; export const Collapsed: Story = {
args: {
export const Expanded = Template.bind({}); expanded: false,
Expanded.args = { },
expanded: true,
};
export const Collapsed = Template.bind({});
Collapsed.args = {
expanded: false,
}; };

View File

@ -1,28 +1,29 @@
import { ComponentMeta, Story } from "@storybook/react"; import { FormFooter } from "./FormFooter";
import { FormFooter, FormFooterProps } from "./FormFooter"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof FormFooter> = {
title: "components/FormFooter", title: "components/FormFooter",
component: FormFooter, component: FormFooter,
argTypes: { };
onCancel: { action: "cancel" },
export default meta;
type Story = StoryObj<typeof FormFooter>;
export const Ready: Story = {
args: {
isLoading: false,
}, },
} as ComponentMeta<typeof FormFooter>;
const Template: Story<FormFooterProps> = (args) => <FormFooter {...args} />;
export const Ready = Template.bind({});
Ready.args = {
isLoading: false,
}; };
export const Custom = Template.bind({}); export const Custom: Story = {
Custom.args = { args: {
isLoading: false, isLoading: false,
submitLabel: "Create", submitLabel: "Create",
},
}; };
export const Loading = Template.bind({}); export const Loading: Story = {
Loading.args = { args: {
isLoading: true, isLoading: true,
},
}; };

View File

@ -1,17 +1,12 @@
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { action } from "@storybook/addon-actions"; import { action } from "@storybook/addon-actions";
import { ComponentMeta, Story } from "@storybook/react";
import { FormFooter } from "../FormFooter/FormFooter"; import { FormFooter } from "../FormFooter/FormFooter";
import { Stack } from "../Stack/Stack"; import { Stack } from "../Stack/Stack";
import { FullPageForm, FullPageFormProps } from "./FullPageForm"; import { FullPageForm, FullPageFormProps } from "./FullPageForm";
import type { Meta, StoryObj } from "@storybook/react";
export default { const Template = (props: FullPageFormProps) => (
title: "components/FullPageForm", <FullPageForm {...props}>
component: FullPageForm,
} as ComponentMeta<typeof FullPageForm>;
const Template: Story<FullPageFormProps> = (args) => (
<FullPageForm {...args}>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
@ -26,8 +21,17 @@ const Template: Story<FullPageFormProps> = (args) => (
</FullPageForm> </FullPageForm>
); );
export const Example = Template.bind({}); const meta: Meta<typeof FullPageForm> = {
Example.args = { title: "components/FullPageForm",
title: "My Form", component: Template,
detail: "Lorem ipsum dolor", };
export default meta;
type Story = StoryObj<typeof FullPageForm>;
export const Example: Story = {
args: {
title: "My Form",
detail: "Lorem ipsum dolor",
},
}; };

View File

@ -0,0 +1,35 @@
import { EnterpriseSnackbar } from "./EnterpriseSnackbar";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof EnterpriseSnackbar> = {
title: "components/EnterpriseSnackbar",
component: EnterpriseSnackbar,
};
export default meta;
type Story = StoryObj<typeof EnterpriseSnackbar>;
export const Error: Story = {
args: {
variant: "error",
open: true,
message: "Oops, something wrong happened.",
},
};
export const Info: Story = {
args: {
variant: "info",
open: true,
message: "Hey, something happened.",
},
};
export const Success: Story = {
args: {
variant: "success",
open: true,
message: "Hey, something good happened.",
},
};

View File

@ -5,7 +5,7 @@ import Snackbar, {
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { FC } from "react"; import { FC } from "react";
import { combineClasses } from "../../../utils/combineClasses"; import { combineClasses } from "utils/combineClasses";
type EnterpriseSnackbarVariant = "error" | "info" | "success"; type EnterpriseSnackbarVariant = "error" | "info" | "success";

View File

@ -1,35 +0,0 @@
import { Story } from "@storybook/react";
import {
EnterpriseSnackbar,
EnterpriseSnackbarProps,
} from "./EnterpriseSnackbar";
export default {
title: "components/EnterpriseSnackbar",
component: EnterpriseSnackbar,
};
const Template: Story<EnterpriseSnackbarProps> = (
args: EnterpriseSnackbarProps,
) => <EnterpriseSnackbar {...args} />;
export const Error = Template.bind({});
Error.args = {
variant: "error",
open: true,
message: "Oops, something wrong happened.",
};
export const Info = Template.bind({});
Info.args = {
variant: "info",
open: true,
message: "Hey, something happened.",
};
export const Success = Template.bind({});
Success.args = {
variant: "success",
open: true,
message: "Hey, something good happened.",
};

View File

@ -1,8 +1,8 @@
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { useCallback, useState, FC } from "react"; import { useCallback, useState, FC } from "react";
import { useCustomEvent } from "../../hooks/events"; import { useCustomEvent } from "hooks/events";
import { CustomEventListener } from "../../utils/events"; import { CustomEventListener } from "utils/events";
import { EnterpriseSnackbar } from "./EnterpriseSnackbar/EnterpriseSnackbar"; import { EnterpriseSnackbar } from "./EnterpriseSnackbar";
import { ErrorIcon } from "../Icons/ErrorIcon"; import { ErrorIcon } from "../Icons/ErrorIcon";
import { Typography } from "../Typography/Typography"; import { Typography } from "../Typography/Typography";
import { import {

View File

@ -1,15 +1,17 @@
import { Story } from "@storybook/react"; import { GroupAvatar } from "./GroupAvatar";
import { GroupAvatar, GroupAvatarProps } from "./GroupAvatar"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof GroupAvatar> = {
title: "components/GroupAvatar", title: "components/GroupAvatar",
component: GroupAvatar, component: GroupAvatar,
}; };
const Template: Story<GroupAvatarProps> = (args) => <GroupAvatar {...args} />; export default meta;
type Story = StoryObj<typeof GroupAvatar>;
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
name: "My Group", name: "My Group",
avatarURL: "", avatarURL: "",
},
}; };

View File

@ -1,38 +1,36 @@
import { ComponentMeta, Story } from "@storybook/react";
import { import {
HelpTooltip, HelpTooltip,
HelpTooltipLink, HelpTooltipLink,
HelpTooltipLinksGroup, HelpTooltipLinksGroup,
HelpTooltipProps,
HelpTooltipText, HelpTooltipText,
HelpTooltipTitle, HelpTooltipTitle,
} from "./HelpTooltip"; } from "./HelpTooltip";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof HelpTooltip> = {
title: "components/HelpTooltip", title: "components/HelpTooltip",
component: HelpTooltip, component: HelpTooltip,
} as ComponentMeta<typeof HelpTooltip>; args: {
children: (
const Template: Story<HelpTooltipProps> = (args) => ( <>
<HelpTooltip {...args}> <HelpTooltipTitle>What is a template?</HelpTooltipTitle>
<HelpTooltipTitle>What is a template?</HelpTooltipTitle> <HelpTooltipText>
<HelpTooltipText> A template is a common configuration for your team&apos;s workspaces.
A template is a common configuration for your team&apos;s workspaces. </HelpTooltipText>
</HelpTooltipText> <HelpTooltipLinksGroup>
<HelpTooltipLinksGroup> <HelpTooltipLink href="https://github.com/coder/coder/">
<HelpTooltipLink href="https://github.com/coder/coder/"> Creating a template
Creating a template </HelpTooltipLink>
</HelpTooltipLink> <HelpTooltipLink href="https://github.com/coder/coder/">
<HelpTooltipLink href="https://github.com/coder/coder/"> Updating a template
Updating a template </HelpTooltipLink>
</HelpTooltipLink> </HelpTooltipLinksGroup>
</HelpTooltipLinksGroup> </>
</HelpTooltip> ),
); },
export const Close = Template.bind({});
export const Open = Template.bind({});
Open.args = {
open: true,
}; };
export default meta;
type Story = StoryObj<typeof HelpTooltip>;
export const Example: Story = {};

View File

@ -1 +0,0 @@
export * from "./HelpTooltip";

View File

@ -1,7 +1,7 @@
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import InputAdornment from "@mui/material/InputAdornment"; import InputAdornment from "@mui/material/InputAdornment";
import Popover from "@mui/material/Popover"; import Popover from "@mui/material/Popover";
import TextField from "@mui/material/TextField"; import TextField, { TextFieldProps } from "@mui/material/TextField";
import { OpenDropdown } from "components/DropdownArrows/DropdownArrows"; import { OpenDropdown } from "components/DropdownArrows/DropdownArrows";
import { useRef, FC, useState } from "react"; import { useRef, FC, useState } from "react";
import Picker from "@emoji-mart/react"; import Picker from "@emoji-mart/react";
@ -9,9 +9,12 @@ import { makeStyles } from "@mui/styles";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import data from "@emoji-mart/data/sets/14/twitter.json"; import data from "@emoji-mart/data/sets/14/twitter.json";
import { IconFieldProps } from "./types";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
type IconFieldProps = TextFieldProps & {
onPickEmoji: (value: string) => void;
};
const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => { const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
if ( if (
typeof textFieldProps.value !== "string" && typeof textFieldProps.value !== "string" &&

View File

@ -1,9 +1,8 @@
import { lazy, FC, Suspense } from "react"; import { lazy, Suspense, ComponentProps } from "react";
import { IconFieldProps } from "./types";
const IconField = lazy(() => import("./IconField")); const IconField = lazy(() => import("./IconField"));
export const LazyIconField: FC<IconFieldProps> = (props) => { export const LazyIconField = (props: ComponentProps<typeof IconField>) => {
return ( return (
<Suspense fallback={<div role="progressbar" data-testid="loader" />}> <Suspense fallback={<div role="progressbar" data-testid="loader" />}>
<IconField {...props} /> <IconField {...props} />

View File

@ -1,5 +0,0 @@
import { TextFieldProps } from "@mui/material/TextField";
export type IconFieldProps = TextFieldProps & {
onPickEmoji: (value: string) => void;
};

View File

@ -1,28 +1,25 @@
import { Story } from "@storybook/react"; import { LoadingButton } from "./LoadingButton";
import { LoadingButton, LoadingButtonProps } from "./LoadingButton"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof LoadingButton> = {
title: "components/LoadingButton", title: "components/LoadingButton",
component: LoadingButton, component: LoadingButton,
argTypes: {
loading: { control: "boolean" },
children: { control: "text" },
},
args: { args: {
children: "Create workspace", children: "Create workspace",
}, },
}; };
const Template: Story<LoadingButtonProps> = (args) => ( export default meta;
<LoadingButton {...args} /> type Story = StoryObj<typeof LoadingButton>;
);
export const Loading = Template.bind({}); export const Loading: Story = {
Loading.args = { args: {
loading: true, loading: true,
},
}; };
export const NotLoading = Template.bind({}); export const NotLoading: Story = {
NotLoading.args = { args: {
loading: false, loading: false,
},
}; };

View File

@ -1,17 +1,20 @@
import { ComponentMeta, Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { Margins } from "./Margins"; import { Margins } from "./Margins";
export default { const meta: Meta<typeof Margins> = {
title: "components/Margins", title: "components/Margins",
component: Margins, component: Margins,
} as ComponentMeta<typeof Margins>; };
const Template: Story = (args) => ( export default meta;
<Margins {...args}> type Story = StoryObj<typeof Margins>;
<div style={{ width: "100%", background: "black" }}>
Here is some content that will not get too wide!
</div>
</Margins>
);
export const Example = Template.bind({}); export const Example: Story = {
args: {
children: (
<div style={{ width: "100%", background: "black" }}>
Here is some content that will not get too wide!
</div>
),
},
};

View File

@ -1,18 +1,17 @@
import { ComponentMeta, Story } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { Markdown, MarkdownProps } from "./Markdown"; import { Markdown } from "./Markdown";
export default { const meta: Meta<typeof Markdown> = {
title: "components/Markdown", title: "components/Markdown",
component: Markdown, component: Markdown,
} as ComponentMeta<typeof Markdown>; };
const Template: Story<MarkdownProps> = ({ children }) => ( export default meta;
<Markdown>{children}</Markdown> type Story = StoryObj<typeof Markdown>;
);
export const WithCode = Template.bind({}); export const WithCode: Story = {
WithCode.args = { args: {
children: ` children: `
## Required permissions / policy ## Required permissions / policy
The following sample policy allows Coder to create EC2 instances and modify instances provisioned by Coder: The following sample policy allows Coder to create EC2 instances and modify instances provisioned by Coder:
@ -64,12 +63,14 @@ WithCode.args = {
] ]
} }
\`\`\``, \`\`\``,
},
}; };
export const WithTable = Template.bind({}); export const WithTable: Story = {
WithTable.args = { args: {
children: ` children: `
| heading | b | c | d | | heading | b | c | d |
| - | :- | -: | :-: | | - | :- | -: | :-: |
| cell 1 | cell 2 | 3 | 4 | `, | cell 1 | cell 2 | 3 | 4 | `,
},
}; };

View File

@ -1,26 +1,29 @@
import { ComponentMeta, Story } from "@storybook/react";
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "./PageHeader"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "./PageHeader";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof PageHeader> = {
title: "components/PageHeader", title: "components/PageHeader",
component: PageHeader, component: PageHeader,
} as ComponentMeta<typeof PageHeader>; };
const WithTitleTemplate: Story = () => ( export default meta;
<PageHeader> type Story = StoryObj<typeof PageHeader>;
<PageHeaderTitle>Templates</PageHeaderTitle>
</PageHeader>
);
export const WithTitle = WithTitleTemplate.bind({}); export const WithTitle: Story = {
args: {
children: <PageHeaderTitle>Templates</PageHeaderTitle>,
},
};
const WithSubtitleTemplate: Story = () => ( export const WithSubtitle: Story = {
<PageHeader> args: {
<PageHeaderTitle>Templates</PageHeaderTitle> children: (
<PageHeaderSubtitle> <>
Create a new workspace from a Template <PageHeaderTitle>Templates</PageHeaderTitle>
</PageHeaderSubtitle> <PageHeaderSubtitle>
</PageHeader> Create a new workspace from a Template
); </PageHeaderSubtitle>
</>
export const WithSubtitle = WithSubtitleTemplate.bind({}); ),
},
};

View File

@ -1,8 +1,8 @@
import { Story } from "@storybook/react"; import { PaginationWidget } from "./PaginationWidget";
import { PaginationWidget, PaginationWidgetProps } from "./PaginationWidget";
import { createPaginationRef } from "./utils"; import { createPaginationRef } from "./utils";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof PaginationWidget> = {
title: "components/PaginationWidget", title: "components/PaginationWidget",
component: PaginationWidget, component: PaginationWidget,
args: { args: {
@ -13,28 +13,31 @@ export default {
}, },
}; };
const Template: Story<PaginationWidgetProps> = ( export default meta;
args: PaginationWidgetProps, type Story = StoryObj<typeof PaginationWidget>;
) => <PaginationWidget {...args} />;
export const LessThan8Pages = Template.bind({}); export const MoreThan8Pages: Story = {};
LessThan8Pages.args = {
numRecords: 84, export const LessThan8Pages: Story = {
args: {
numRecords: 84,
},
}; };
export const MoreThan8Pages = Template.bind({}); export const MoreThan7PagesWithActivePageCloseToStart: Story = {
args: {
export const MoreThan7PagesWithActivePageCloseToStart = Template.bind({}); paginationRef: createPaginationRef({ page: 2, limit: 12 }),
MoreThan7PagesWithActivePageCloseToStart.args = { },
paginationRef: createPaginationRef({ page: 2, limit: 12 }),
}; };
export const MoreThan7PagesWithActivePageFarFromBoundaries = Template.bind({}); export const MoreThan7PagesWithActivePageFarFromBoundaries: Story = {
MoreThan7PagesWithActivePageFarFromBoundaries.args = { args: {
paginationRef: createPaginationRef({ page: 4, limit: 12 }), paginationRef: createPaginationRef({ page: 4, limit: 12 }),
},
}; };
export const MoreThan7PagesWithActivePageCloseToEnd = Template.bind({}); export const MoreThan7PagesWithActivePageCloseToEnd: Story = {
MoreThan7PagesWithActivePageCloseToEnd.args = { args: {
paginationRef: createPaginationRef({ page: 17, limit: 12 }), paginationRef: createPaginationRef({ page: 17, limit: 12 }),
},
}; };

View File

@ -1,55 +1,9 @@
import { screen } from "@testing-library/react"; import { screen } from "@testing-library/react";
import { render } from "../../testHelpers/renderHelpers"; import { render } from "testHelpers/renderHelpers";
import { PaginationWidget } from "./PaginationWidget"; import { PaginationWidget } from "./PaginationWidget";
import { createPaginationRef } from "./utils"; import { createPaginationRef } from "./utils";
describe("PaginatedList", () => { describe("PaginatedList", () => {
it("displays an accessible previous and next button", () => {
render(
<PaginationWidget
prevLabel="Previous"
nextLabel="Next"
paginationRef={createPaginationRef({ page: 2, limit: 12 })}
numRecords={200}
/>,
);
expect(screen.getByRole("button", { name: "Previous page" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
});
it("displays the expected number of pages with one ellipsis tile", () => {
const { container } = render(
<PaginationWidget
prevLabel="Previous"
nextLabel="Next"
numRecords={200}
paginationRef={createPaginationRef({ page: 1, limit: 12 })}
/>,
);
// 7 total spaces. 6 are page numbers, one is ellipsis
expect(
container.querySelectorAll(`button[name="Page button"]`),
).toHaveLength(6);
});
it("displays the expected number of pages with two ellipsis tiles", () => {
const { container } = render(
<PaginationWidget
prevLabel="Previous"
nextLabel="Next"
numRecords={200}
paginationRef={createPaginationRef({ page: 6, limit: 12 })}
/>,
);
// 7 total spaces. 2 sets of ellipsis on either side of the active page
expect(
container.querySelectorAll(`button[name="Page button"]`),
).toHaveLength(5);
});
it("disables the previous button on the first page", () => { it("disables the previous button on the first page", () => {
render( render(
<PaginationWidget <PaginationWidget

View File

@ -1,57 +1,66 @@
import { Story } from "@storybook/react"; import { Pill } from "./Pill";
import { Pill, PillProps } from "./Pill"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof Pill> = {
title: "components/Pill", title: "components/Pill",
component: Pill, component: Pill,
}; };
const Template: Story<PillProps> = (args) => <Pill {...args} />; export default meta;
type Story = StoryObj<typeof Pill>;
export const Primary = Template.bind({}); export const Primary: Story = {
Primary.args = { args: {
text: "Primary", text: "Primary",
type: "primary", type: "primary",
},
}; };
export const Secondary = Template.bind({}); export const Secondary: Story = {
Secondary.args = { args: {
text: "Secondary", text: "Secondary",
type: "secondary", type: "secondary",
},
}; };
export const Success = Template.bind({}); export const Success: Story = {
Success.args = { args: {
text: "Success", text: "Success",
type: "success", type: "success",
},
}; };
export const Info = Template.bind({}); export const Info: Story = {
Info.args = { args: {
text: "Information", text: "Information",
type: "info", type: "info",
},
}; };
export const Warning = Template.bind({}); export const Warning: Story = {
Warning.args = { args: {
text: "Warning", text: "Warning",
type: "warning", type: "warning",
},
}; };
export const Error = Template.bind({}); export const Error: Story = {
Error.args = { args: {
text: "Error", text: "Error",
type: "error", type: "error",
},
}; };
export const Default = Template.bind({}); export const Default: Story = {
Default.args = { args: {
text: "Default", text: "Default",
},
}; };
export const WarningLight = Template.bind({}); export const WarningLight: Story = {
WarningLight.args = { args: {
text: "Warning", text: "Warning",
type: "warning", type: "warning",
lightBorder: true, lightBorder: true,
},
}; };

View File

@ -1,18 +1,17 @@
import { Story } from "@storybook/react";
import { import {
WorkspaceAgentMetadataDescription, WorkspaceAgentMetadataDescription,
WorkspaceAgentMetadataResult, WorkspaceAgentMetadataResult,
} from "api/typesGenerated"; } from "api/typesGenerated";
import { AgentMetadataView, AgentMetadataViewProps } from "./AgentMetadata"; import { AgentMetadataView } from "./AgentMetadata";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof AgentMetadataView> = {
title: "components/AgentMetadata", title: "components/AgentMetadataView",
component: AgentMetadataView, component: AgentMetadataView,
}; };
const Template: Story<AgentMetadataViewProps> = (args) => ( export default meta;
<AgentMetadataView {...args} /> type Story = StoryObj<typeof AgentMetadataView>;
);
const resultDefaults: WorkspaceAgentMetadataResult = { const resultDefaults: WorkspaceAgentMetadataResult = {
collected_at: "2021-05-05T00:00:00Z", collected_at: "2021-05-05T00:00:00Z",
@ -29,79 +28,80 @@ const descriptionDefaults: WorkspaceAgentMetadataDescription = {
script: "some command", script: "some command",
}; };
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
metadata: [ metadata: [
{ {
result: { result: {
...resultDefaults, ...resultDefaults,
value: "110%", value: "110%",
},
description: {
...descriptionDefaults,
display_name: "CPU",
key: "CPU",
},
}, },
description: { {
...descriptionDefaults, result: {
display_name: "CPU", ...resultDefaults,
key: "CPU", value: "50GB",
},
description: {
...descriptionDefaults,
display_name: "Memory",
key: "Memory",
},
}, },
}, {
{ result: {
result: { ...resultDefaults,
...resultDefaults, value: "stale value",
value: "50GB", age: 300,
},
description: {
...descriptionDefaults,
interval: 5,
display_name: "Stale",
key: "stale",
},
}, },
description: { {
...descriptionDefaults, result: {
display_name: "Memory", ...resultDefaults,
key: "Memory", value: "oops",
error: "fatal error",
},
description: {
...descriptionDefaults,
display_name: "Error",
key: "error",
},
}, },
}, {
{ result: {
result: { ...resultDefaults,
...resultDefaults, value: "",
value: "stale value", collected_at: "0001-01-01T00:00:00Z",
age: 300, age: 1000000,
},
description: {
...descriptionDefaults,
display_name: "Never loads",
key: "nloads",
},
}, },
description: { {
...descriptionDefaults, result: {
interval: 5, ...resultDefaults,
display_name: "Stale", value: "r".repeat(1000),
key: "stale", },
description: {
...descriptionDefaults,
display_name: "Really, really big",
key: "big",
},
}, },
}, ],
{ },
result: {
...resultDefaults,
value: "oops",
error: "fatal error",
},
description: {
...descriptionDefaults,
display_name: "Error",
key: "error",
},
},
{
result: {
...resultDefaults,
value: "",
collected_at: "0001-01-01T00:00:00Z",
age: 1000000,
},
description: {
...descriptionDefaults,
display_name: "Never loads",
key: "nloads",
},
},
{
result: {
...resultDefaults,
value: "r".repeat(1000),
},
description: {
...descriptionDefaults,
display_name: "Really, really big",
key: "big",
},
},
],
}; };

View File

@ -1,4 +1,3 @@
import { Story } from "@storybook/react";
import { import {
MockPrimaryWorkspaceProxy, MockPrimaryWorkspaceProxy,
MockWorkspaceProxies, MockWorkspaceProxies,
@ -18,65 +17,9 @@ import {
MockWorkspaceApp, MockWorkspaceApp,
MockProxyLatencies, MockProxyLatencies,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { AgentRow, AgentRowProps } from "./AgentRow"; import { AgentRow } from "./AgentRow";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import { Region } from "api/typesGenerated"; import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "components/AgentRow",
component: AgentRow,
args: {
storybookStartupLogs: [
"\x1b[91mCloning Git repository...",
"\x1b[2;37;41mStarting Docker Daemon...",
"\x1b[1;95mAdding some 🧙magic🧙...",
"Starting VS Code...",
"\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 1475 0 1475 0 0 4231 0 --:--:-- --:--:-- --:--:-- 4238",
].map((line, index) => ({
id: index,
level: "info",
output: line,
time: "",
})),
},
};
const Template: Story<AgentRowProps> = (args) => {
return TemplateFC(args, [], undefined);
};
const TemplateWithPortForward: Story<AgentRowProps> = (args) => {
return TemplateFC(args, MockWorkspaceProxies, MockPrimaryWorkspaceProxy);
};
const TemplateFC = (
args: AgentRowProps,
proxies: Region[],
selectedProxy?: Region,
) => {
return (
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy(proxies, selectedProxy),
proxies: proxies,
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<AgentRow {...args} />
</ProxyContext.Provider>
);
};
const defaultAgentMetadata = [ const defaultAgentMetadata = [
{ {
@ -141,135 +84,205 @@ const defaultAgentMetadata = [
}, },
]; ];
export const Example = Template.bind({}); const meta: Meta<typeof AgentRow> = {
Example.args = { title: "components/AgentRow",
agent: { component: AgentRow,
...MockWorkspaceAgent, args: {
startup_script: storybookLogs: [
'set -eux -o pipefail\n\n# install and start code-server\ncurl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3\n/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n\n\nif [ ! -d ~/coder ]; then\n mkdir -p ~/coder\n\n git clone https://github.com/coder/coder ~/coder\nfi\n\nsudo service docker start\nDOTFILES_URI=" "\nrm -f ~/.personalize.log\nif [ -n "${DOTFILES_URI// }" ]; then\n coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log\nfi\nif [ -x ~/personalize ]; then\n ~/personalize 2>&1 | tee -a ~/.personalize.log\nelif [ -f ~/personalize ]; then\n echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log\nfi\n', "\x1b[91mCloning Git repository...",
"\x1b[2;37;41mStarting Docker Daemon...",
"\x1b[1;95mAdding some 🧙magic🧙...",
"Starting VS Code...",
"\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 1475 0 1475 0 0 4231 0 --:--:-- --:--:-- --:--:-- 4238",
].map((line, index) => ({
id: index,
level: "info",
output: line,
time: "",
})),
agent: {
...MockWorkspaceAgent,
startup_script:
'set -eux -o pipefail\n\n# install and start code-server\ncurl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3\n/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n\n\nif [ ! -d ~/coder ]; then\n mkdir -p ~/coder\n\n git clone https://github.com/coder/coder ~/coder\nfi\n\nsudo service docker start\nDOTFILES_URI=" "\nrm -f ~/.personalize.log\nif [ -n "${DOTFILES_URI// }" ]; then\n coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log\nfi\nif [ -x ~/personalize ]; then\n ~/personalize 2>&1 | tee -a ~/.personalize.log\nelif [ -f ~/personalize ]; then\n echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log\nfi\n',
},
workspace: MockWorkspace,
showApps: true,
storybookAgentMetadata: defaultAgentMetadata,
}, },
workspace: MockWorkspace, decorators: [
showApps: true, (Story) => (
storybookAgentMetadata: defaultAgentMetadata, <ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy([], undefined),
proxies: [],
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<Story />
</ProxyContext.Provider>
),
],
}; };
export const HideSSHButton = Template.bind({}); export default meta;
HideSSHButton.args = { type Story = StoryObj<typeof AgentRow>;
...Example.args,
hideSSHButton: true,
};
export const HideVSCodeDesktopButton = Template.bind({}); export const Example: Story = {};
HideVSCodeDesktopButton.args = {
...Example.args,
hideVSCodeDesktopButton: true,
};
export const NotShowingApps = Template.bind({}); export const HideSSHButton: Story = {
NotShowingApps.args = { args: {
...Example.args, hideSSHButton: true,
showApps: false,
};
export const BunchOfApps = Template.bind({});
BunchOfApps.args = {
...Example.args,
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
workspace: MockWorkspace,
showApps: true,
};
export const Connecting = Template.bind({});
Connecting.args = {
...Example.args,
agent: MockWorkspaceAgentConnecting,
storybookAgentMetadata: [],
};
export const Timeout = Template.bind({});
Timeout.args = {
...Example.args,
agent: MockWorkspaceAgentTimeout,
};
export const Starting = Template.bind({});
Starting.args = {
...Example.args,
agent: MockWorkspaceAgentStarting,
};
export const Started = Template.bind({});
Started.args = {
...Example.args,
agent: {
...MockWorkspaceAgentReady,
logs_length: 1,
}, },
}; };
export const StartedNoMetadata = Template.bind({}); export const HideVSCodeDesktopButton: Story = {
StartedNoMetadata.args = { args: {
...Started.args, hideVSCodeDesktopButton: true,
storybookAgentMetadata: [], },
}; };
export const StartTimeout = Template.bind({}); export const NotShowingApps: Story = {
StartTimeout.args = { args: {
...Example.args, showApps: false,
agent: MockWorkspaceAgentStartTimeout, },
}; };
export const StartError = Template.bind({}); export const BunchOfApps: Story = {
StartError.args = { args: {
...Example.args, agent: {
agent: MockWorkspaceAgentStartError, ...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
workspace: MockWorkspace,
showApps: true,
},
}; };
export const ShuttingDown = Template.bind({}); export const Connecting: Story = {
ShuttingDown.args = { args: {
...Example.args, agent: MockWorkspaceAgentConnecting,
agent: MockWorkspaceAgentShuttingDown, storybookAgentMetadata: [],
},
}; };
export const ShutdownTimeout = Template.bind({}); export const Timeout: Story = {
ShutdownTimeout.args = { args: {
...Example.args, agent: MockWorkspaceAgentTimeout,
agent: MockWorkspaceAgentShutdownTimeout, },
}; };
export const ShutdownError = Template.bind({}); export const Starting: Story = {
ShutdownError.args = { args: {
...Example.args, agent: MockWorkspaceAgentStarting,
agent: MockWorkspaceAgentShutdownError, },
}; };
export const Off = Template.bind({}); export const Started: Story = {
Off.args = { args: {
...Example.args, agent: {
agent: MockWorkspaceAgentOff, ...MockWorkspaceAgentReady,
logs_length: 1,
},
},
}; };
export const ShowingPortForward = TemplateWithPortForward.bind({}); export const StartedNoMetadata: Story = {
ShowingPortForward.args = { args: {
...Example.args, ...Started.args,
storybookAgentMetadata: [],
},
}; };
export const Outdated = Template.bind({}); export const StartTimeout: Story = {
Outdated.args = { args: {
...Example.args, agent: MockWorkspaceAgentStartTimeout,
agent: MockWorkspaceAgentOutdated, },
workspace: MockWorkspace, };
serverVersion: "v99.999.9999+c1cdf14",
export const StartError: Story = {
args: {
agent: MockWorkspaceAgentStartError,
},
};
export const ShuttingDown: Story = {
args: {
agent: MockWorkspaceAgentShuttingDown,
},
};
export const ShutdownTimeout: Story = {
args: {
agent: MockWorkspaceAgentShutdownTimeout,
},
};
export const ShutdownError: Story = {
args: {
agent: MockWorkspaceAgentShutdownError,
},
};
export const Off: Story = {
args: {
agent: MockWorkspaceAgentOff,
},
};
export const ShowingPortForward: Story = {
decorators: [
(Story) => (
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy(
MockWorkspaceProxies,
MockPrimaryWorkspaceProxy,
),
proxies: MockWorkspaceProxies,
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<Story />
</ProxyContext.Provider>
),
],
};
export const Outdated: Story = {
args: {
agent: MockWorkspaceAgentOutdated,
workspace: MockWorkspace,
serverVersion: "v99.999.9999+c1cdf14",
},
}; };

View File

@ -7,10 +7,7 @@ import {
CloseDropdown, CloseDropdown,
OpenDropdown, OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"; } from "components/DropdownArrows/DropdownArrows";
import { import { LogLine, logLineHeight } from "components/WorkspaceBuildLogs/Logs";
LogLine,
logLineHeight,
} from "components/WorkspaceBuildLogs/Logs/Logs";
import { PortForwardButton } from "./PortForwardButton"; import { PortForwardButton } from "./PortForwardButton";
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton"; import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
import { import {

View File

@ -1,44 +1,43 @@
import { Story } from "@storybook/react";
import { MockWorkspaceAgent, MockWorkspaceApp } from "testHelpers/entities"; import { MockWorkspaceAgent, MockWorkspaceApp } from "testHelpers/entities";
import { AgentRowPreview, AgentRowPreviewProps } from "./AgentRowPreview"; import { AgentRowPreview } from "./AgentRowPreview";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof AgentRowPreview> = {
title: "components/AgentRowPreview", title: "components/AgentRowPreview",
component: AgentRowPreview, component: AgentRowPreview,
}; args: {
agent: MockWorkspaceAgent,
const Template: Story<AgentRowPreviewProps> = (args) => (
<AgentRowPreview {...args} />
);
export const Example = Template.bind({});
Example.args = {
agent: MockWorkspaceAgent,
};
export const BunchOfApps = Template.bind({});
BunchOfApps.args = {
...Example.args,
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
}, },
}; };
export const NoApps = Template.bind({}); export default meta;
NoApps.args = { type Story = StoryObj<typeof AgentRowPreview>;
...Example.args,
agent: { export const Example: Story = {};
...MockWorkspaceAgent,
apps: [], export const BunchOfApps: Story = {
args: {
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
},
};
export const NoApps: Story = {
args: {
agent: {
...MockWorkspaceAgent,
apps: [],
},
}, },
}; };

View File

@ -1,4 +1,3 @@
import { Story } from "@storybook/react";
import { import {
MockPrimaryWorkspaceProxy, MockPrimaryWorkspaceProxy,
MockWorkspaceProxies, MockWorkspaceProxies,
@ -7,116 +6,132 @@ import {
MockWorkspaceApp, MockWorkspaceApp,
MockProxyLatencies, MockProxyLatencies,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { AppLink, AppLinkProps } from "./AppLink"; import { AppLink } from "./AppLink";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof AppLink> = {
title: "components/AppLink", title: "components/AppLink",
component: AppLink, component: AppLink,
decorators: [
(Story) => (
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy(
MockWorkspaceProxies,
MockPrimaryWorkspaceProxy,
),
proxies: MockWorkspaceProxies,
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<Story />
</ProxyContext.Provider>
),
],
}; };
const Template: Story<AppLinkProps> = (args) => ( export default meta;
<ProxyContext.Provider type Story = StoryObj<typeof AppLink>;
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy(MockWorkspaceProxies, MockPrimaryWorkspaceProxy),
proxies: MockWorkspaceProxies,
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<AppLink {...args} />
</ProxyContext.Provider>
);
export const WithIcon = Template.bind({}); export const WithIcon: Story = {
WithIcon.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
icon: "/icon/code.svg", icon: "/icon/code.svg",
sharing_level: "owner", sharing_level: "owner",
health: "healthy", health: "healthy",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const ExternalApp = Template.bind({}); export const ExternalApp: Story = {
ExternalApp.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
external: true, external: true,
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const SharingLevelOwner = Template.bind({}); export const SharingLevelOwner: Story = {
SharingLevelOwner.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
sharing_level: "owner", sharing_level: "owner",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const SharingLevelAuthenticated = Template.bind({}); export const SharingLevelAuthenticated: Story = {
SharingLevelAuthenticated.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
sharing_level: "authenticated", sharing_level: "authenticated",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const SharingLevelPublic = Template.bind({}); export const SharingLevelPublic: Story = {
SharingLevelPublic.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
sharing_level: "public", sharing_level: "public",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const HealthDisabled = Template.bind({}); export const HealthDisabled: Story = {
HealthDisabled.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
sharing_level: "owner", sharing_level: "owner",
health: "disabled", health: "disabled",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const HealthInitializing = Template.bind({}); export const HealthInitializing: Story = {
HealthInitializing.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
health: "initializing", health: "initializing",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };
export const HealthUnhealthy = Template.bind({}); export const HealthUnhealthy: Story = {
HealthUnhealthy.args = { args: {
workspace: MockWorkspace, workspace: MockWorkspace,
app: { app: {
...MockWorkspaceApp, ...MockWorkspaceApp,
health: "unhealthy", health: "unhealthy",
},
agent: MockWorkspaceAgent,
}, },
agent: MockWorkspaceAgent,
}; };

View File

@ -8,7 +8,7 @@ import {
HelpTooltipLinksGroup, HelpTooltipLinksGroup,
HelpTooltipText, HelpTooltipText,
HelpTooltipTitle, HelpTooltipTitle,
} from "components/HelpTooltip"; } from "components/HelpTooltip/HelpTooltip";
import { SecondaryAgentButton } from "components/Resources/AgentButton"; import { SecondaryAgentButton } from "components/Resources/AgentButton";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";

View File

@ -1,61 +1,66 @@
import { Story } from "@storybook/react";
import { MockWorkspaceResource } from "testHelpers/entities"; import { MockWorkspaceResource } from "testHelpers/entities";
import { ResourceAvatar, ResourceAvatarProps } from "./ResourceAvatar"; import { ResourceAvatar } from "./ResourceAvatar";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof ResourceAvatar> = {
title: "components/ResourceAvatar", title: "components/ResourceAvatar",
component: ResourceAvatar, component: ResourceAvatar,
}; };
const Template: Story<ResourceAvatarProps> = (args) => ( export default meta;
<ResourceAvatar {...args} /> type Story = StoryObj<typeof ResourceAvatar>;
);
export const VolumeResource = Template.bind({}); export const VolumeResource: Story = {
VolumeResource.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "docker_volume", type: "docker_volume",
},
}, },
}; };
export const ComputeResource = Template.bind({}); export const ComputeResource: Story = {
ComputeResource.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "docker_container", type: "docker_container",
},
}, },
}; };
export const ImageResource = Template.bind({}); export const ImageResource: Story = {
ImageResource.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "docker_image", type: "docker_image",
},
}, },
}; };
export const NullResource = Template.bind({}); export const NullResource: Story = {
NullResource.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "null_resource", type: "null_resource",
},
}, },
}; };
export const UnknownResource = Template.bind({}); export const UnknownResource: Story = {
UnknownResource.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "noexistentvalue", type: "noexistentvalue",
},
}, },
}; };
export const EmptyIcon = Template.bind({}); export const EmptyIcon: Story = {
EmptyIcon.args = { args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
type: "helm_release", type: "helm_release",
icon: "", icon: "",
},
}, },
}; };

View File

@ -1,124 +1,129 @@
import { action } from "@storybook/addon-actions"; import { action } from "@storybook/addon-actions";
import { Story } from "@storybook/react";
import { import {
MockProxyLatencies, MockProxyLatencies,
MockWorkspace, MockWorkspace,
MockWorkspaceResource, MockWorkspaceResource,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { AgentRow } from "./AgentRow"; import { AgentRow } from "./AgentRow";
import { ResourceCard, ResourceCardProps } from "./ResourceCard"; import { ResourceCard } from "./ResourceCard";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof ResourceCard> = {
title: "components/ResourceCard", title: "components/ResourceCard",
component: ResourceCard, component: ResourceCard,
}; args: {
resource: MockWorkspaceResource,
const Template: Story<ResourceCardProps> = (args) => <ResourceCard {...args} />; agentRow: (agent) => (
<ProxyContext.Provider
export const Example = Template.bind({}); value={{
Example.args = { proxyLatencies: MockProxyLatencies,
resource: MockWorkspaceResource, proxy: getPreferredProxy([], undefined),
agentRow: (agent) => ( proxies: [],
<ProxyContext.Provider isLoading: false,
value={{ isFetched: true,
proxyLatencies: MockProxyLatencies, setProxy: () => {
proxy: getPreferredProxy([], undefined), return;
proxies: [], },
isLoading: false, clearProxy: () => {
isFetched: true, return;
setProxy: () => { },
return; refetchProxyLatencies: (): Date => {
}, return new Date();
clearProxy: () => { },
return; }}
}, >
refetchProxyLatencies: (): Date => { <AgentRow
return new Date(); showApps
}, key={agent.id}
}} agent={agent}
> workspace={MockWorkspace}
<AgentRow serverVersion=""
showApps onUpdateAgent={action("updateAgent")}
key={agent.id} />
agent={agent} </ProxyContext.Provider>
workspace={MockWorkspace} ),
serverVersion="" },
onUpdateAgent={action("updateAgent")} };
/>
</ProxyContext.Provider> export default meta;
), type Story = StoryObj<typeof ResourceCard>;
};
export const Example: Story = {};
export const BunchOfMetadata = Template.bind({});
BunchOfMetadata.args = { export const BunchOfMetadata: Story = {
...Example.args, args: {
resource: { resource: {
...MockWorkspaceResource, ...MockWorkspaceResource,
metadata: [ metadata: [
{ {
key: "CPU(limits, requests)", key: "CPU(limits, requests)",
value: "2 cores, 500m", value: "2 cores, 500m",
sensitive: false, sensitive: false,
}, },
{ key: "container image pull policy", value: "Always", sensitive: false }, {
{ key: "Disk", value: "10GiB", sensitive: false }, key: "container image pull policy",
{ value: "Always",
key: "image", sensitive: false,
value: "docker.io/markmilligan/pycharm-community:latest", },
sensitive: false, { key: "Disk", value: "10GiB", sensitive: false },
}, {
{ key: "kubernetes namespace", value: "oss", sensitive: false }, key: "image",
{ value: "docker.io/markmilligan/pycharm-community:latest",
key: "memory(limits, requests)", sensitive: false,
value: "4GB, 500mi", },
sensitive: false, { key: "kubernetes namespace", value: "oss", sensitive: false },
}, {
{ key: "memory(limits, requests)",
key: "security context - container", value: "4GB, 500mi",
value: "run_as_user 1000", sensitive: false,
sensitive: false, },
}, {
{ key: "security context - container",
key: "security context - pod", value: "run_as_user 1000",
value: "run_as_user 1000 fs_group 1000", sensitive: false,
sensitive: false, },
}, {
{ key: "volume", value: "/home/coder", sensitive: false }, key: "security context - pod",
{ value: "run_as_user 1000 fs_group 1000",
key: "secret", sensitive: false,
value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI", },
sensitive: true, { key: "volume", value: "/home/coder", sensitive: false },
}, {
], key: "secret",
value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI",
sensitive: true,
},
],
},
agentRow: (agent) => (
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy([], undefined),
proxies: [],
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<AgentRow
showApps
key={agent.id}
agent={agent}
workspace={MockWorkspace}
serverVersion=""
onUpdateAgent={action("updateAgent")}
/>
</ProxyContext.Provider>
),
}, },
agentRow: (agent) => (
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy([], undefined),
proxies: [],
isLoading: false,
isFetched: true,
setProxy: () => {
return;
},
clearProxy: () => {
return;
},
refetchProxyLatencies: (): Date => {
return new Date();
},
}}
>
<AgentRow
showApps
key={agent.id}
agent={agent}
workspace={MockWorkspace}
serverVersion=""
onUpdateAgent={action("updateAgent")}
/>
</ProxyContext.Provider>
),
}; };

View File

@ -1,25 +1,28 @@
import { Story } from "@storybook/react";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { SSHButton, SSHButtonProps } from "./SSHButton"; import { SSHButton } from "./SSHButton";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof SSHButton> = {
title: "components/SSHButton", title: "components/SSHButton",
component: SSHButton, component: SSHButton,
}; };
const Template: Story<SSHButtonProps> = (args) => <SSHButton {...args} />; export default meta;
type Story = StoryObj<typeof SSHButton>;
export const Closed = Template.bind({}); export const Closed: Story = {
Closed.args = { args: {
workspaceName: MockWorkspace.name, workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name, agentName: MockWorkspaceAgent.name,
sshPrefix: "coder.", sshPrefix: "coder.",
},
}; };
export const Opened = Template.bind({}); export const Opened: Story = {
Opened.args = { args: {
workspaceName: MockWorkspace.name, workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name, agentName: MockWorkspaceAgent.name,
defaultIsOpen: true, defaultIsOpen: true,
sshPrefix: "coder.", sshPrefix: "coder.",
},
}; };

View File

@ -1,15 +1,17 @@
import { Story } from "@storybook/react";
import { MockWorkspace } from "testHelpers/entities"; import { MockWorkspace } from "testHelpers/entities";
import { TerminalLink, TerminalLinkProps } from "./TerminalLink"; import { TerminalLink } from "./TerminalLink";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof TerminalLink> = {
title: "components/TerminalLink", title: "components/TerminalLink",
component: TerminalLink, component: TerminalLink,
}; };
const Template: Story<TerminalLinkProps> = (args) => <TerminalLink {...args} />; export default meta;
type Story = StoryObj<typeof TerminalLink>;
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
workspaceName: MockWorkspace.name, workspaceName: MockWorkspace.name,
},
}; };

View File

@ -1,29 +1,26 @@
import { Story } from "@storybook/react";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { import { VSCodeDesktopButton } from "./VSCodeDesktopButton";
VSCodeDesktopButton, import type { Meta, StoryObj } from "@storybook/react";
VSCodeDesktopButtonProps,
} from "./VSCodeDesktopButton";
export default { const meta: Meta<typeof VSCodeDesktopButton> = {
title: "components/VSCodeDesktopButton", title: "components/VSCodeDesktopButton",
component: VSCodeDesktopButton, component: VSCodeDesktopButton,
}; };
const Template: Story<VSCodeDesktopButtonProps> = (args) => ( export default meta;
<VSCodeDesktopButton {...args} /> type Story = StoryObj<typeof VSCodeDesktopButton>;
);
export const Default = Template.bind({}); export const Default: Story = {
Default.args = { args: {
userName: MockWorkspace.owner_name, userName: MockWorkspace.owner_name,
workspaceName: MockWorkspace.name, workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name, agentName: MockWorkspaceAgent.name,
displayApps: [ displayApps: [
"vscode", "vscode",
"port_forwarding_helper", "port_forwarding_helper",
"ssh_helper", "ssh_helper",
"vscode_insiders", "vscode_insiders",
"web_terminal", "web_terminal",
], ],
},
}; };

View File

@ -1,16 +0,0 @@
import { Story } from "@storybook/react";
import { useState } from "react";
import { MultiTextField, MultiTextFieldProps } from "./MultiTextField";
export default {
title: "components/MultiTextField",
component: MultiTextField,
};
const Template: Story<MultiTextFieldProps> = (args) => {
const [values, setValues] = useState(args.values ?? ["foo", "bar"]);
return <MultiTextField {...args} values={values} onChange={setValues} />;
};
export const Example = Template.bind({});
Example.args = {};

View File

@ -5,10 +5,10 @@ import { makeStyles } from "@mui/styles";
import TextField, { TextFieldProps } from "@mui/material/TextField"; import TextField, { TextFieldProps } from "@mui/material/TextField";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { FC } from "react"; import { FC } from "react";
import { TemplateVersionParameter } from "../../api/typesGenerated"; import { TemplateVersionParameter } from "api/typesGenerated";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
import { MemoizedMarkdown } from "components/Markdown/Markdown"; import { MemoizedMarkdown } from "components/Markdown/Markdown";
import { MultiTextField } from "components/RichParameterInput/MultiTextField/MultiTextField"; import { MultiTextField } from "./MultiTextField";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";

View File

@ -1,21 +0,0 @@
import TextField from "@mui/material/TextField";
import { Story } from "@storybook/react";
import { Stack, StackProps } from "./Stack";
export default {
title: "components/Stack",
component: Stack,
};
const Template: Story<StackProps> = (args: StackProps) => (
<Stack {...args}>
<TextField autoFocus autoComplete="name" fullWidth label="Name" />
<TextField autoComplete="email" fullWidth label="Email" />
<TextField autoComplete="username" fullWidth label="Username" />
</Stack>
);
export const Example = Template.bind({});
Example.args = {
spacing: 2,
};

View File

@ -1,26 +1,22 @@
import { ComponentMeta, Story } from "@storybook/react"; import { TableRowMenu } from "./TableRowMenu";
import { TableRowMenu, TableRowMenuProps } from "./TableRowMenu"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof TableRowMenu> = {
title: "components/TableRowMenu", title: "components/TableRowMenu",
component: TableRowMenu, component: TableRowMenu,
} as ComponentMeta<typeof TableRowMenu>;
type DataType = {
id: string;
}; };
const Template: Story<TableRowMenuProps<DataType>> = (args) => ( export default meta;
<TableRowMenu {...args} /> type Story = StoryObj<typeof TableRowMenu<{ id: string }>>;
);
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
data: { id: "123" }, data: { id: "123" },
menuItems: [ menuItems: [
{ label: "Suspend", onClick: (data) => alert(data.id), disabled: false }, { label: "Suspend", onClick: (data) => alert(data.id), disabled: false },
{ label: "Update", onClick: (data) => alert(data.id), disabled: false }, { label: "Update", onClick: (data) => alert(data.id), disabled: false },
{ label: "Delete", onClick: (data) => alert(data.id), disabled: false }, { label: "Delete", onClick: (data) => alert(data.id), disabled: false },
{ label: "Explode", onClick: (data) => alert(data.id), disabled: true }, { label: "Explode", onClick: (data) => alert(data.id), disabled: true },
], ],
},
}; };

View File

@ -1,12 +1,9 @@
import { ComponentMeta, Story } from "@storybook/react";
import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"; import { MockTemplate, MockTemplateVersion } from "testHelpers/entities";
import { import { TemplatePageHeader } from "./TemplatePageHeader";
TemplatePageHeader, import type { Meta, StoryObj } from "@storybook/react";
TemplatePageHeaderProps,
} from "./TemplatePageHeader";
export default { const meta: Meta<typeof TemplatePageHeader> = {
title: "Components/TemplatePageHeader", title: "components/TemplatePageHeader",
component: TemplatePageHeader, component: TemplatePageHeader,
args: { args: {
template: MockTemplate, template: MockTemplate,
@ -15,18 +12,17 @@ export default {
canUpdateTemplate: true, canUpdateTemplate: true,
}, },
}, },
} as ComponentMeta<typeof TemplatePageHeader>; };
const Template: Story<TemplatePageHeaderProps> = (args) => ( export default meta;
<TemplatePageHeader {...args} /> type Story = StoryObj<typeof TemplatePageHeader>;
);
export const CanUpdate = Template.bind({}); export const CanUpdate: Story = {};
CanUpdate.args = {};
export const CanNotUpdate = Template.bind({}); export const CanNotUpdate: Story = {
CanNotUpdate.args = { args: {
permissions: { permissions: {
canUpdateTemplate: false, canUpdateTemplate: false,
},
}, },
}; };

View File

@ -1,19 +1,16 @@
import { Story } from "@storybook/react"; import { TemplateVersionWarnings } from "./TemplateVersionWarnings";
import { import type { Meta, StoryObj } from "@storybook/react";
TemplateVersionWarnings,
TemplateVersionWarningsProps,
} from "./TemplateVersionWarnings";
export default { const meta: Meta<typeof TemplateVersionWarnings> = {
title: "components/TemplateVersionWarnings", title: "components/TemplateVersionWarnings",
component: TemplateVersionWarnings, component: TemplateVersionWarnings,
}; };
const Template: Story<TemplateVersionWarningsProps> = (args) => ( export default meta;
<TemplateVersionWarnings {...args} /> type Story = StoryObj<typeof TemplateVersionWarnings>;
);
export const UnsupportedWorkspaces = Template.bind({}); export const UnsupportedWorkspaces: Story = {
UnsupportedWorkspaces.args = { args: {
warnings: ["UNSUPPORTED_WORKSPACES"], warnings: ["UNSUPPORTED_WORKSPACES"],
},
}; };

View File

@ -1,25 +1,25 @@
import { Story } from "@storybook/react"; import { Typography } from "./Typography";
import { Typography, TypographyProps } from "./Typography"; import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof Typography> = {
title: "components/Typography", title: "components/Typography",
component: Typography, component: Typography,
args: {
children: "Colorless green ideas sleep furiously",
},
}; };
const Template: Story<TypographyProps> = (args: TypographyProps) => ( export default meta;
<> type Story = StoryObj<typeof Typography>;
<Typography {...args}>Colorless green ideas sleep furiously</Typography>
<Typography {...args}>
More people have been to France than I have
</Typography>
</>
);
export const Short = Template.bind({}); export const Short: Story = {
Short.args = { args: {
short: true, short: true,
},
}; };
export const Tall = Template.bind({});
Tall.args = { export const Tall: Story = {
short: false, args: {
short: false,
},
}; };

View File

@ -1,23 +1,24 @@
import { Story } from "@storybook/react";
import { MockUser } from "testHelpers/entities"; import { MockUser } from "testHelpers/entities";
import { UserAutocomplete, UserAutocompleteProps } from "./UserAutocomplete"; import { UserAutocomplete } from "./UserAutocomplete";
import type { Meta, StoryObj } from "@storybook/react";
export default { const meta: Meta<typeof UserAutocomplete> = {
title: "components/UserAutocomplete", title: "components/UserAutocomplete",
component: UserAutocomplete, component: UserAutocomplete,
}; };
const Template: Story<UserAutocompleteProps> = ( export default meta;
args: UserAutocompleteProps, type Story = StoryObj<typeof UserAutocomplete>;
) => <UserAutocomplete {...args} />;
export const Example = Template.bind({}); export const Example: Story = {
Example.args = { args: {
value: MockUser, value: MockUser,
label: "User", label: "User",
},
}; };
export const NoLabel = Template.bind({}); export const NoLabel: Story = {
NoLabel.args = { args: {
value: MockUser, value: MockUser,
},
}; };

View File

@ -2,10 +2,9 @@ import { makeStyles } from "@mui/styles";
import { LogLevel } from "api/typesGenerated"; import { LogLevel } from "api/typesGenerated";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { FC, useMemo } from "react"; import { FC, useMemo } from "react";
import { MONOSPACE_FONT_FAMILY } from "../../../theme/constants"; import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { combineClasses } from "../../../utils/combineClasses"; import { combineClasses } from "utils/combineClasses";
import AnsiToHTML from "ansi-to-html"; import AnsiToHTML from "ansi-to-html";
import { Theme } from "@mui/material/styles";
export interface Line { export interface Line {
time: string; time: string;
@ -16,19 +15,15 @@ export interface Line {
export interface LogsProps { export interface LogsProps {
lines: Line[]; lines: Line[];
hideTimestamps?: boolean; hideTimestamps?: boolean;
lineNumbers?: boolean;
className?: string; className?: string;
} }
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
hideTimestamps, hideTimestamps,
lines, lines,
lineNumbers,
className = "", className = "",
}) => { }) => {
const styles = useStyles({ const styles = useStyles();
lineNumbers: Boolean(lineNumbers),
});
return ( return (
<div className={combineClasses([className, styles.root])}> <div className={combineClasses([className, styles.root])}>
@ -38,9 +33,7 @@ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
{!hideTimestamps && ( {!hideTimestamps && (
<> <>
<span className={styles.time}> <span className={styles.time}>
{lineNumbers {dayjs(line.time).format(`HH:mm:ss.SSS`)}
? idx + 1
: dayjs(line.time).format(`HH:mm:ss.SSS`)}
</span> </span>
<span className={styles.space} /> <span className={styles.space} />
</> </>
@ -63,9 +56,7 @@ export const LogLine: FC<{
number?: number; number?: number;
style?: React.CSSProperties; style?: React.CSSProperties;
}> = ({ line, hideTimestamp, number, style }) => { }> = ({ line, hideTimestamp, number, style }) => {
const styles = useStyles({ const styles = useStyles();
lineNumbers: Boolean(number),
});
const output = useMemo(() => { const output = useMemo(() => {
return convert.toHtml(line.output.split(/\r/g).pop() as string); return convert.toHtml(line.output.split(/\r/g).pop() as string);
}, [line.output]); }, [line.output]);
@ -89,12 +80,7 @@ export const LogLine: FC<{
); );
}; };
const useStyles = makeStyles< const useStyles = makeStyles((theme) => ({
Theme,
{
lineNumbers: boolean;
}
>((theme) => ({
root: { root: {
minHeight: 156, minHeight: 156,
padding: theme.spacing(1, 0), padding: theme.spacing(1, 0),
@ -116,7 +102,7 @@ const useStyles = makeStyles<
fontSize: 14, fontSize: 14,
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontFamily: MONOSPACE_FONT_FAMILY, fontFamily: MONOSPACE_FONT_FAMILY,
height: ({ lineNumbers }) => (lineNumbers ? logLineHeight : "auto"), height: "auto",
// Whitespace is significant in terminal output for alignment // Whitespace is significant in terminal output for alignment
whiteSpace: "pre", whiteSpace: "pre",
padding: theme.spacing(0, 4), padding: theme.spacing(0, 4),
@ -141,7 +127,7 @@ const useStyles = makeStyles<
}, },
time: { time: {
userSelect: "none", userSelect: "none",
width: ({ lineNumbers }) => theme.spacing(lineNumbers ? 3.5 : 12.5), width: theme.spacing(12.5),
whiteSpace: "pre", whiteSpace: "pre",
display: "inline-block", display: "inline-block",
color: theme.palette.text.secondary, color: theme.palette.text.secondary,

View File

@ -1,27 +0,0 @@
import { ComponentMeta, Story } from "@storybook/react";
import { LogLevel } from "api/typesGenerated";
import { MockWorkspaceBuildLogs } from "../../../testHelpers/entities";
import { Logs, LogsProps } from "./Logs";
export default {
title: "components/Logs",
component: Logs,
} as ComponentMeta<typeof Logs>;
const Template: Story<LogsProps> = (args) => <Logs {...args} />;
const lines = MockWorkspaceBuildLogs.map((log) => ({
time: log.created_at,
output: log.output,
level: "info" as LogLevel,
}));
export const Example = Template.bind({});
Example.args = {
lines,
};
export const WithLineNumbers = Template.bind({});
WithLineNumbers.args = {
lines,
lineNumbers: true,
};

View File

@ -1,37 +0,0 @@
import { ProvisionerJobLog } from "api/typesGenerated";
import { groupLogsByStage } from "./WorkspaceBuildLogs";
describe("groupLogsByStage", () => {
it("should group them by stage", () => {
const input: ProvisionerJobLog[] = [
{
id: 1,
created_at: "oct 13",
log_source: "provisioner",
log_level: "debug",
stage: "build",
output: "test",
},
{
id: 2,
created_at: "oct 13",
log_source: "provisioner",
log_level: "debug",
stage: "cleanup",
output: "test",
},
{
id: 3,
created_at: "oct 13",
log_source: "provisioner",
log_level: "debug",
stage: "cleanup",
output: "done",
},
];
const actual = groupLogsByStage(input);
expect(actual["cleanup"].length).toBe(2);
});
});

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import { ComponentProps, FC, Fragment } from "react"; import { ComponentProps, FC, Fragment } from "react";
import { ProvisionerJobLog } from "../../api/typesGenerated"; import { ProvisionerJobLog } from "../../api/typesGenerated";
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"; import { MONOSPACE_FONT_FAMILY } from "../../theme/constants";
import { Logs } from "./Logs/Logs"; import { Logs } from "./Logs";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { combineClasses } from "utils/combineClasses"; import { combineClasses } from "utils/combineClasses";

View File

@ -1,4 +1,3 @@
import { Story } from "@storybook/react";
import { import {
MockCanceledWorkspace, MockCanceledWorkspace,
MockCancelingWorkspace, MockCancelingWorkspace,
@ -15,16 +14,9 @@ import {
MockExperiments, MockExperiments,
MockAppearance, MockAppearance,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
WorkspaceStatusBadge,
WorkspaceStatusBadgeProps,
} from "./WorkspaceStatusBadge";
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; import { DashboardProviderContext } from "components/Dashboard/DashboardProvider";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "components/WorkspaceStatusBadge",
component: WorkspaceStatusBadge,
};
const MockedAppearance = { const MockedAppearance = {
config: MockAppearance, config: MockAppearance,
@ -33,65 +25,84 @@ const MockedAppearance = {
save: () => null, save: () => null,
}; };
const Template: Story<WorkspaceStatusBadgeProps> = (args) => ( const meta: Meta<typeof WorkspaceStatusBadge> = {
<DashboardProviderContext.Provider title: "components/WorkspaceStatusBadge",
value={{ component: WorkspaceStatusBadge,
buildInfo: MockBuildInfo, decorators: [
entitlements: MockEntitlementsWithScheduling, (Story) => (
experiments: MockExperiments, <DashboardProviderContext.Provider
appearance: MockedAppearance, value={{
}} buildInfo: MockBuildInfo,
> entitlements: MockEntitlementsWithScheduling,
<WorkspaceStatusBadge {...args} /> experiments: MockExperiments,
</DashboardProviderContext.Provider> appearance: MockedAppearance,
); }}
>
export const Running = Template.bind({}); <Story />
Running.args = { </DashboardProviderContext.Provider>
workspace: MockWorkspace, ),
],
}; };
export const Starting = Template.bind({}); export default meta;
Starting.args = { type Story = StoryObj<typeof WorkspaceStatusBadge>;
workspace: MockStartingWorkspace,
export const Running: Story = {
args: {
workspace: MockWorkspace,
},
}; };
export const Stopped = Template.bind({}); export const Starting: Story = {
Stopped.args = { args: {
workspace: MockStoppedWorkspace, workspace: MockStartingWorkspace,
},
}; };
export const Stopping = Template.bind({}); export const Stopped: Story = {
Stopping.args = { args: {
workspace: MockStoppingWorkspace, workspace: MockStoppedWorkspace,
},
}; };
export const Deleting = Template.bind({}); export const Stopping: Story = {
Deleting.args = { args: {
workspace: MockDeletingWorkspace, workspace: MockStoppingWorkspace,
},
}; };
export const Deleted = Template.bind({}); export const Deleting: Story = {
Deleted.args = { args: {
workspace: MockDeletedWorkspace, workspace: MockDeletingWorkspace,
},
}; };
export const Canceling = Template.bind({}); export const Deleted: Story = {
Canceling.args = { args: {
workspace: MockCancelingWorkspace, workspace: MockDeletedWorkspace,
},
}; };
export const Canceled = Template.bind({}); export const Canceling: Story = {
Canceled.args = { args: {
workspace: MockCanceledWorkspace, workspace: MockCancelingWorkspace,
},
}; };
export const Failed = Template.bind({}); export const Canceled: Story = {
Failed.args = { args: {
workspace: MockFailedWorkspace, workspace: MockCanceledWorkspace,
},
}; };
export const Pending = Template.bind({}); export const Failed: Story = {
Pending.args = { args: {
workspace: MockPendingWorkspace, workspace: MockFailedWorkspace,
},
};
export const Pending: Story = {
args: {
workspace: MockPendingWorkspace,
},
}; };

View File

@ -1,6 +1,6 @@
import * as API from "api/api"; import * as API from "api/api";
import { createMachine, assign } from "xstate"; import { createMachine, assign } from "xstate";
import { Line } from "components/WorkspaceBuildLogs/Logs/Logs"; import { Line } from "components/WorkspaceBuildLogs/Logs";
// Logs are stored as the Line interface to make rendering // Logs are stored as the Line interface to make rendering
// much more efficient. Instead of mapping objects each time, we're // much more efficient. Instead of mapping objects each time, we're