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 Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof Alert> = {
@ -21,7 +20,6 @@ export const Success: Story = {
args: {
children: "You're doing great!",
severity: "success",
onRetry: undefined,
},
};
@ -56,14 +54,3 @@ export const WarningWithActionAndDismiss: Story = {
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 & {
actions?: ReactNode;
dismissible?: boolean;
onRetry?: () => void;
onDismiss?: () => void;
};
export const Alert: FC<AlertProps> = ({
children,
actions,
onRetry,
dismissible,
severity,
onDismiss,
@ -34,13 +32,6 @@ export const Alert: FC<AlertProps> = ({
{/* CTAs passed in by the consumer */}
{actions}
{/* retry CTA */}
{onRetry && (
<Button variant="text" size="small" onClick={onRetry}>
Retry
</Button>
)}
{/* close CTA */}
{dismissible && (
<Button

View File

@ -1,7 +1,6 @@
import Button from "@mui/material/Button";
import { mockApiError } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import { ErrorAlert } from "./ErrorAlert";
const mockError = mockApiError({
@ -15,7 +14,6 @@ const meta: Meta<typeof ErrorAlert> = {
args: {
error: mockError,
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 = {
args: {
error: new Error("Non API error here"),

View File

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

View File

@ -4,7 +4,6 @@ import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { makeStyles } from "@mui/styles";
import { FC } from "react";
import { combineClasses } from "utils/combineClasses";
import { firstLetter } from "./firstLetter";
export type AvatarProps = MuiAvatarProps & {
size?: "sm" | "md" | "xl";
@ -32,7 +31,6 @@ export const Avatar: FC<AvatarProps> = ({
fitImage && styles.fitImage,
])}
>
{/* If the children is a string, we always want to render the first letter */}
{typeof children === "string" ? firstLetter(children) : children}
</MuiAvatar>
);
@ -46,6 +44,14 @@ export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
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) => ({
// Size styles
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 { AvatarData, AvatarDataProps } from "./AvatarData";
import type { Meta, StoryObj } from "@storybook/react";
import { AvatarData } from "./AvatarData";
export default {
const meta: Meta<typeof AvatarData> = {
title: "components/AvatarData",
component: AvatarData,
args: {
title: "coder",
subtitle: "coder@coder.com",
},
};
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => (
<AvatarData {...args} />
);
export default meta;
type Story = StoryObj<typeof AvatarData>;
export const Example = Template.bind({});
Example.args = {
title: "coder",
subtitle: "coder@coder.com",
};
export const WithTitleAndSubtitle: Story = {};
export const WithImage = Template.bind({});
WithImage.args = {
title: "coder",
subtitle: "coder@coder.com",
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
export const WithImage: Story = {
args: {
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
},
};

View File

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

View File

@ -1,9 +1,9 @@
import { Story } from "@storybook/react";
import { CodeExample, CodeExampleProps } from "./CodeExample";
import type { Meta, StoryObj } from "@storybook/react";
import { CodeExample } from "./CodeExample";
const sampleCode = `echo "Hello, world"`;
export default {
const meta: Meta<typeof CodeExample> = {
title: "components/CodeExample",
component: CodeExample,
argTypes: {
@ -11,16 +11,17 @@ export default {
},
};
const Template: Story<CodeExampleProps> = (args: CodeExampleProps) => (
<CodeExample {...args} />
);
export default meta;
type Story = StoryObj<typeof CodeExample>;
export const Example = Template.bind({});
Example.args = {
code: sampleCode,
export const Example: Story = {
args: {
code: sampleCode,
},
};
export const LongCode = Template.bind({});
LongCode.args = {
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
export const LongCode: Story = {
args: {
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 {
code: string;
className?: string;
buttonClassName?: string;
tooltipTitle?: string;
inline?: boolean;
password?: boolean;
className?: string;
}
/**
* Component to show single-line code examples, with a copy button
*/
export const CodeExample: FC<React.PropsWithChildren<CodeExampleProps>> = ({
export const CodeExample: FC<CodeExampleProps> = ({
code,
password,
className,
buttonClassName,
tooltipTitle,
inline,
}) => {
const styles = useStyles({ inline: inline });
const styles = useStyles({ password });
return (
<div className={combineClasses([styles.root, className])}>
<code className={styles.code}>{code}</code>
<CopyButton
text={code}
tooltipTitle={tooltipTitle}
buttonClassName={buttonClassName}
/>
<CopyButton text={code} />
</div>
);
};
@ -48,14 +39,14 @@ const useStyles = makeStyles<Theme, styleProps>((theme) => ({
display: props.inline ? "inline-flex" : "flex",
flexDirection: "row",
alignItems: "center",
background: props.inline ? "rgb(0 0 0 / 30%)" : "hsl(223, 27%, 3%)",
border: props.inline ? undefined : `1px solid ${theme.palette.divider}`,
background: "rgb(0 0 0 / 30%)",
color: theme.palette.primary.contrastText,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(1),
lineHeight: "150%",
border: `1px solid ${theme.palette.divider}`,
}),
code: {
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 {
DeploymentBannerView,
DeploymentBannerViewProps,
} from "./DeploymentBannerView";
import { DeploymentBannerView } from "./DeploymentBannerView";
export default {
const meta: Meta<typeof DeploymentBannerView> = {
title: "components/DeploymentBannerView",
component: DeploymentBannerView,
args: {
stats: MockDeploymentStats,
},
};
const Template: Story<DeploymentBannerViewProps> = (args) => (
<DeploymentBannerView {...args} />
);
export default meta;
type Story = StoryObj<typeof DeploymentBannerView>;
export const Preview = Template.bind({});
Preview.args = {
stats: MockDeploymentStats,
};
export const Preview: Story = {};

View File

@ -1,34 +1,36 @@
import { Story } from "@storybook/react";
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView";
import type { Meta, StoryObj } from "@storybook/react";
import { LicenseBannerView } from "./LicenseBannerView";
export default {
const meta: Meta<typeof LicenseBannerView> = {
title: "components/LicenseBannerView",
component: LicenseBannerView,
};
const Template: Story<LicenseBannerViewProps> = (args) => (
<LicenseBannerView {...args} />
);
export default meta;
type Story = StoryObj<typeof LicenseBannerView>;
export const OneWarning = Template.bind({});
OneWarning.args = {
errors: [],
warnings: ["You have exceeded the number of seats in your license."],
export const OneWarning: Story = {
args: {
errors: [],
warnings: ["You have exceeded the number of seats in your license."],
},
};
export const TwoWarnings = Template.bind({});
TwoWarnings.args = {
errors: [],
warnings: [
"You have exceeded the number of seats in your license.",
"You are flying too close to the sun.",
],
export const TwoWarnings: Story = {
args: {
errors: [],
warnings: [
"You have exceeded the number of seats in your license.",
"You are flying too close to the sun.",
],
},
};
export const OneError = Template.bind({});
OneError.args = {
errors: [
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.",
],
warnings: [],
export const OneError: Story = {
args: {
errors: [
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.",
],
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 { NavbarView, NavbarViewProps } from "./NavbarView";
import { NavbarView } from "./NavbarView";
export default {
const meta: Meta<typeof NavbarView> = {
title: "components/NavbarView",
component: NavbarView,
argTypes: {
onSignOut: { action: "Sign Out" },
args: {
user: MockUser,
},
};
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => (
<NavbarView {...args} />
);
export default meta;
type Story = StoryObj<typeof NavbarView>;
export const ForAdmin = Template.bind({});
ForAdmin.args = {
user: MockUser,
onSignOut: () => {
return Promise.resolve();
export const ForAdmin: Story = {};
export const ForMember: Story = {
args: {
user: MockUser2,
canViewAuditLog: false,
canViewDeployment: false,
canViewAllUsers: false,
},
};
export const ForMember = Template.bind({});
ForMember.args = {
user: MockUser2,
onSignOut: () => {
return Promise.resolve();
export const SmallViewport: Story = {
parameters: {
viewport: {
defaultViewport: "tablet",
},
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 {
MockPrimaryWorkspaceProxy,
MockUser,
MockUser2,
} from "../../../testHelpers/entities";
import { renderWithAuth } from "../../../testHelpers/renderHelpers";
import { Language as navLanguage, NavbarView } from "./NavbarView";
@ -28,18 +27,6 @@ describe("NavbarView", () => {
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 () => {
renderWithAuth(
<NavbarView
@ -85,32 +72,6 @@ describe("NavbarView", () => {
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 () => {
renderWithAuth(
<NavbarView
@ -126,21 +87,6 @@ describe("NavbarView", () => {
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 () => {
renderWithAuth(
<NavbarView
@ -157,19 +103,4 @@ describe("NavbarView", () => {
"/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 { FC } from "react";
import { NavLink } from "react-router-dom";
import { ellipsizeText } from "../../../../../utils/ellipsizeText";
import { Typography } from "../../../../Typography/Typography";
import { ellipsizeText } from "utils/ellipsizeText";
import { Typography } from "components/Typography/Typography";
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 { 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",
component: UserDropdown,
argTypes: {
onSignOut: { action: "Sign Out" },
args: {
user: MockUser,
},
};
const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => (
<Box style={{ backgroundColor: "#000", width: 88 }}>
<UserDropdown {...args} />
</Box>
);
export default meta;
type Story = StoryObj<typeof UserDropdown>;
export const Example = Template.bind({});
Example.args = {
user: MockUser,
onSignOut: () => {
return Promise.resolve();
},
};
export const Example: Story = {};

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 { useState, FC, PropsWithChildren, MouseEvent } from "react";
import { colors } from "theme/colors";
import * as TypesGen from "../../../../api/typesGenerated";
import { navHeight } from "../../../../theme/constants";
import { BorderedMenu } from "./BorderedMenu/BorderedMenu";
import * as TypesGen from "api/typesGenerated";
import { navHeight } from "theme/constants";
import { BorderedMenu } from "./BorderedMenu";
import {
CloseDropdown,
OpenDropdown,
} from "../../../DropdownArrows/DropdownArrows";
import { UserAvatar } from "../../../UserAvatar/UserAvatar";
import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent";
} from "components/DropdownArrows/DropdownArrows";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { UserDropdownContent } from "./UserDropdownContent";
import { BUTTON_SM_HEIGHT } from "theme/theme";
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 { FC } from "react";
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 LogoutIcon from "@mui/icons-material/ExitToAppOutlined";
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 { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView";
import type { Meta, StoryObj } from "@storybook/react";
import { ServiceBannerView } from "./ServiceBannerView";
export default {
const meta: Meta<typeof ServiceBannerView> = {
title: "components/ServiceBannerView",
component: ServiceBannerView,
};
const Template: Story<ServiceBannerViewProps> = (args) => (
<ServiceBannerView {...args} />
);
export default meta;
type Story = StoryObj<typeof ServiceBannerView>;
export const Production = Template.bind({});
Production.args = {
message: "weeeee",
backgroundColor: "#FFFFFF",
export const Production: Story = {
args: {
message: "weeeee",
backgroundColor: "#FFFFFF",
},
};
export const Preview = Template.bind({});
Preview.args = {
message: "weeeee",
backgroundColor: "#000000",
preview: true,
export const Preview: Story = {
args: {
message: "weeeee",
backgroundColor: "#000000",
preview: true,
},
};

View File

@ -12,7 +12,7 @@ import {
OptionValue,
} from "components/DeploySettingsLayout/Option";
import { FC } from "react";
import { intervalToDuration, formatDuration } from "date-fns";
import { optionValue } from "./optionValue";
const OptionsTable: FC<{
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) => ({
table: {
"& td": {

View File

@ -1,4 +1,4 @@
import { optionValue } from "./OptionsTable";
import { optionValue } from "./optionValue";
import { DeploymentOption } from "api/types";
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 { ComponentMeta, Story } from "@storybook/react";
import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog";
import type { Meta, StoryObj } from "@storybook/react";
import { ConfirmDialog } from "./ConfirmDialog";
export default {
title: "Components/Dialogs/ConfirmDialog",
const meta: Meta<typeof ConfirmDialog> = {
title: "components/Dialogs/ConfirmDialog",
component: ConfirmDialog,
argTypes: {
open: {
control: "boolean",
},
},
args: {
onClose: action("onClose"),
onConfirm: action("onConfirm"),
open: true,
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({});
InfoDialog.args = {
description: "Information is cool!",
hideCancel: true,
type: "info",
export default meta;
type Story = StoryObj<typeof ConfirmDialog>;
export const Example: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
},
};
export const InfoDialogWithCancel = Template.bind({});
InfoDialogWithCancel.args = {
description: "Information can be cool!",
hideCancel: false,
type: "info",
export const InfoDialog: Story = {
args: {
description: "Information is cool!",
hideCancel: true,
type: "info",
},
};
export const SuccessDialog = Template.bind({});
SuccessDialog.args = {
description: "I am successful.",
hideCancel: true,
type: "success",
export const InfoDialogWithCancel: Story = {
args: {
description: "Information can be cool!",
hideCancel: false,
type: "info",
},
};
export const SuccessDialogWithCancel = Template.bind({});
SuccessDialogWithCancel.args = {
description: "I may be successful.",
hideCancel: false,
type: "success",
export const SuccessDialog: Story = {
args: {
description: "I am successful.",
hideCancel: true,
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 { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog";
import { ConfirmDialog } from "./ConfirmDialog";
import { render } from "testHelpers/renderHelpers";
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", () => {
// Given
const onCloseMock = jest.fn();

View File

@ -1,28 +1,21 @@
import { action } from "@storybook/addon-actions";
import { ComponentMeta, Story } from "@storybook/react";
import { DeleteDialog, DeleteDialogProps } from "./DeleteDialog";
import type { Meta, StoryObj } from "@storybook/react";
import { DeleteDialog } from "./DeleteDialog";
export default {
title: "Components/Dialogs/DeleteDialog",
const meta: Meta<typeof DeleteDialog> = {
title: "components/Dialogs/DeleteDialog",
component: DeleteDialog,
argTypes: {
open: {
control: "boolean",
},
},
args: {
onCancel: action("onClose"),
onConfirm: action("onConfirm"),
open: true,
isOpen: true,
entity: "foo",
name: "MyFoo",
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 { RuntimeErrorState } from "./RuntimeErrorState/RuntimeErrorState";
import { RuntimeErrorState } from "./RuntimeErrorState";
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 { FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { Margins } from "../../Margins/Margins";
import { Margins } from "components/Margins/Margins";
const fetchDynamicallyImportedModuleError =
"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, ExpanderProps } from "./Expander";
import { Expander } from "./Expander";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof Expander> = {
title: "components/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 Expanded = Template.bind({});
Expanded.args = {
expanded: true,
};
export const Collapsed = Template.bind({});
Collapsed.args = {
expanded: false,
export const Collapsed: Story = {
args: {
expanded: false,
},
};

View File

@ -1,28 +1,29 @@
import { ComponentMeta, Story } from "@storybook/react";
import { FormFooter, FormFooterProps } from "./FormFooter";
import { FormFooter } from "./FormFooter";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof FormFooter> = {
title: "components/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({});
Custom.args = {
isLoading: false,
submitLabel: "Create",
export const Custom: Story = {
args: {
isLoading: false,
submitLabel: "Create",
},
};
export const Loading = Template.bind({});
Loading.args = {
isLoading: true,
export const Loading: Story = {
args: {
isLoading: true,
},
};

View File

@ -1,17 +1,12 @@
import TextField from "@mui/material/TextField";
import { action } from "@storybook/addon-actions";
import { ComponentMeta, Story } from "@storybook/react";
import { FormFooter } from "../FormFooter/FormFooter";
import { Stack } from "../Stack/Stack";
import { FullPageForm, FullPageFormProps } from "./FullPageForm";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "components/FullPageForm",
component: FullPageForm,
} as ComponentMeta<typeof FullPageForm>;
const Template: Story<FullPageFormProps> = (args) => (
<FullPageForm {...args}>
const Template = (props: FullPageFormProps) => (
<FullPageForm {...props}>
<form
onSubmit={(e) => {
e.preventDefault();
@ -26,8 +21,17 @@ const Template: Story<FullPageFormProps> = (args) => (
</FullPageForm>
);
export const Example = Template.bind({});
Example.args = {
title: "My Form",
detail: "Lorem ipsum dolor",
const meta: Meta<typeof FullPageForm> = {
title: "components/FullPageForm",
component: Template,
};
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 CloseIcon from "@mui/icons-material/Close";
import { FC } from "react";
import { combineClasses } from "../../../utils/combineClasses";
import { combineClasses } from "utils/combineClasses";
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 { useCallback, useState, FC } from "react";
import { useCustomEvent } from "../../hooks/events";
import { CustomEventListener } from "../../utils/events";
import { EnterpriseSnackbar } from "./EnterpriseSnackbar/EnterpriseSnackbar";
import { useCustomEvent } from "hooks/events";
import { CustomEventListener } from "utils/events";
import { EnterpriseSnackbar } from "./EnterpriseSnackbar";
import { ErrorIcon } from "../Icons/ErrorIcon";
import { Typography } from "../Typography/Typography";
import {

View File

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

View File

@ -1,38 +1,36 @@
import { ComponentMeta, Story } from "@storybook/react";
import {
HelpTooltip,
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipProps,
HelpTooltipText,
HelpTooltipTitle,
} from "./HelpTooltip";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof HelpTooltip> = {
title: "components/HelpTooltip",
component: HelpTooltip,
} as ComponentMeta<typeof HelpTooltip>;
const Template: Story<HelpTooltipProps> = (args) => (
<HelpTooltip {...args}>
<HelpTooltipTitle>What is a template?</HelpTooltipTitle>
<HelpTooltipText>
A template is a common configuration for your team&apos;s workspaces.
</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://github.com/coder/coder/">
Creating a template
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/">
Updating a template
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</HelpTooltip>
);
export const Close = Template.bind({});
export const Open = Template.bind({});
Open.args = {
open: true,
args: {
children: (
<>
<HelpTooltipTitle>What is a template?</HelpTooltipTitle>
<HelpTooltipText>
A template is a common configuration for your team&apos;s workspaces.
</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://github.com/coder/coder/">
Creating a template
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/">
Updating a template
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</>
),
},
};
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 InputAdornment from "@mui/material/InputAdornment";
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 { useRef, FC, useState } from "react";
import Picker from "@emoji-mart/react";
@ -9,9 +9,12 @@ import { makeStyles } from "@mui/styles";
import { colors } from "theme/colors";
import { useTranslation } from "react-i18next";
import data from "@emoji-mart/data/sets/14/twitter.json";
import { IconFieldProps } from "./types";
import { Stack } from "components/Stack/Stack";
type IconFieldProps = TextFieldProps & {
onPickEmoji: (value: string) => void;
};
const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
if (
typeof textFieldProps.value !== "string" &&

View File

@ -1,9 +1,8 @@
import { lazy, FC, Suspense } from "react";
import { IconFieldProps } from "./types";
import { lazy, Suspense, ComponentProps } from "react";
const IconField = lazy(() => import("./IconField"));
export const LazyIconField: FC<IconFieldProps> = (props) => {
export const LazyIconField = (props: ComponentProps<typeof IconField>) => {
return (
<Suspense fallback={<div role="progressbar" data-testid="loader" />}>
<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, LoadingButtonProps } from "./LoadingButton";
import { LoadingButton } from "./LoadingButton";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof LoadingButton> = {
title: "components/LoadingButton",
component: LoadingButton,
argTypes: {
loading: { control: "boolean" },
children: { control: "text" },
},
args: {
children: "Create workspace",
},
};
const Template: Story<LoadingButtonProps> = (args) => (
<LoadingButton {...args} />
);
export default meta;
type Story = StoryObj<typeof LoadingButton>;
export const Loading = Template.bind({});
Loading.args = {
loading: true,
export const Loading: Story = {
args: {
loading: true,
},
};
export const NotLoading = Template.bind({});
NotLoading.args = {
loading: false,
export const NotLoading: Story = {
args: {
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";
export default {
const meta: Meta<typeof Margins> = {
title: "components/Margins",
component: Margins,
} as ComponentMeta<typeof Margins>;
};
const Template: Story = (args) => (
<Margins {...args}>
<div style={{ width: "100%", background: "black" }}>
Here is some content that will not get too wide!
</div>
</Margins>
);
export default meta;
type Story = StoryObj<typeof 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 { Markdown, MarkdownProps } from "./Markdown";
import type { Meta, StoryObj } from "@storybook/react";
import { Markdown } from "./Markdown";
export default {
const meta: Meta<typeof Markdown> = {
title: "components/Markdown",
component: Markdown,
} as ComponentMeta<typeof Markdown>;
};
const Template: Story<MarkdownProps> = ({ children }) => (
<Markdown>{children}</Markdown>
);
export default meta;
type Story = StoryObj<typeof Markdown>;
export const WithCode = Template.bind({});
WithCode.args = {
children: `
export const WithCode: Story = {
args: {
children: `
## Required permissions / policy
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({});
WithTable.args = {
children: `
export const WithTable: Story = {
args: {
children: `
| heading | b | c | d |
| - | :- | -: | :-: |
| 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 type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof PageHeader> = {
title: "components/PageHeader",
component: PageHeader,
} as ComponentMeta<typeof PageHeader>;
};
const WithTitleTemplate: Story = () => (
<PageHeader>
<PageHeaderTitle>Templates</PageHeaderTitle>
</PageHeader>
);
export default meta;
type Story = StoryObj<typeof PageHeader>;
export const WithTitle = WithTitleTemplate.bind({});
export const WithTitle: Story = {
args: {
children: <PageHeaderTitle>Templates</PageHeaderTitle>,
},
};
const WithSubtitleTemplate: Story = () => (
<PageHeader>
<PageHeaderTitle>Templates</PageHeaderTitle>
<PageHeaderSubtitle>
Create a new workspace from a Template
</PageHeaderSubtitle>
</PageHeader>
);
export const WithSubtitle = WithSubtitleTemplate.bind({});
export const WithSubtitle: Story = {
args: {
children: (
<>
<PageHeaderTitle>Templates</PageHeaderTitle>
<PageHeaderSubtitle>
Create a new workspace from a Template
</PageHeaderSubtitle>
</>
),
},
};

View File

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

View File

@ -1,55 +1,9 @@
import { screen } from "@testing-library/react";
import { render } from "../../testHelpers/renderHelpers";
import { render } from "testHelpers/renderHelpers";
import { PaginationWidget } from "./PaginationWidget";
import { createPaginationRef } from "./utils";
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", () => {
render(
<PaginationWidget

View File

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

View File

@ -1,18 +1,17 @@
import { Story } from "@storybook/react";
import {
WorkspaceAgentMetadataDescription,
WorkspaceAgentMetadataResult,
} from "api/typesGenerated";
import { AgentMetadataView, AgentMetadataViewProps } from "./AgentMetadata";
import { AgentMetadataView } from "./AgentMetadata";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "components/AgentMetadata",
const meta: Meta<typeof AgentMetadataView> = {
title: "components/AgentMetadataView",
component: AgentMetadataView,
};
const Template: Story<AgentMetadataViewProps> = (args) => (
<AgentMetadataView {...args} />
);
export default meta;
type Story = StoryObj<typeof AgentMetadataView>;
const resultDefaults: WorkspaceAgentMetadataResult = {
collected_at: "2021-05-05T00:00:00Z",
@ -29,79 +28,80 @@ const descriptionDefaults: WorkspaceAgentMetadataDescription = {
script: "some command",
};
export const Example = Template.bind({});
Example.args = {
metadata: [
{
result: {
...resultDefaults,
value: "110%",
export const Example: Story = {
args: {
metadata: [
{
result: {
...resultDefaults,
value: "110%",
},
description: {
...descriptionDefaults,
display_name: "CPU",
key: "CPU",
},
},
description: {
...descriptionDefaults,
display_name: "CPU",
key: "CPU",
{
result: {
...resultDefaults,
value: "50GB",
},
description: {
...descriptionDefaults,
display_name: "Memory",
key: "Memory",
},
},
},
{
result: {
...resultDefaults,
value: "50GB",
{
result: {
...resultDefaults,
value: "stale value",
age: 300,
},
description: {
...descriptionDefaults,
interval: 5,
display_name: "Stale",
key: "stale",
},
},
description: {
...descriptionDefaults,
display_name: "Memory",
key: "Memory",
{
result: {
...resultDefaults,
value: "oops",
error: "fatal error",
},
description: {
...descriptionDefaults,
display_name: "Error",
key: "error",
},
},
},
{
result: {
...resultDefaults,
value: "stale value",
age: 300,
{
result: {
...resultDefaults,
value: "",
collected_at: "0001-01-01T00:00:00Z",
age: 1000000,
},
description: {
...descriptionDefaults,
display_name: "Never loads",
key: "nloads",
},
},
description: {
...descriptionDefaults,
interval: 5,
display_name: "Stale",
key: "stale",
{
result: {
...resultDefaults,
value: "r".repeat(1000),
},
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 {
MockPrimaryWorkspaceProxy,
MockWorkspaceProxies,
@ -18,65 +17,9 @@ import {
MockWorkspaceApp,
MockProxyLatencies,
} from "testHelpers/entities";
import { AgentRow, AgentRowProps } from "./AgentRow";
import { AgentRow } from "./AgentRow";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import { Region } from "api/typesGenerated";
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>
);
};
import type { Meta, StoryObj } from "@storybook/react";
const defaultAgentMetadata = [
{
@ -141,135 +84,205 @@ const defaultAgentMetadata = [
},
];
export const Example = Template.bind({});
Example.args = {
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',
const meta: Meta<typeof AgentRow> = {
title: "components/AgentRow",
component: AgentRow,
args: {
storybookLogs: [
"\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,
showApps: true,
storybookAgentMetadata: defaultAgentMetadata,
decorators: [
(Story) => (
<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({});
HideSSHButton.args = {
...Example.args,
hideSSHButton: true,
};
export default meta;
type Story = StoryObj<typeof AgentRow>;
export const HideVSCodeDesktopButton = Template.bind({});
HideVSCodeDesktopButton.args = {
...Example.args,
hideVSCodeDesktopButton: true,
};
export const Example: Story = {};
export const NotShowingApps = Template.bind({});
NotShowingApps.args = {
...Example.args,
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 HideSSHButton: Story = {
args: {
hideSSHButton: true,
},
};
export const StartedNoMetadata = Template.bind({});
StartedNoMetadata.args = {
...Started.args,
storybookAgentMetadata: [],
export const HideVSCodeDesktopButton: Story = {
args: {
hideVSCodeDesktopButton: true,
},
};
export const StartTimeout = Template.bind({});
StartTimeout.args = {
...Example.args,
agent: MockWorkspaceAgentStartTimeout,
export const NotShowingApps: Story = {
args: {
showApps: false,
},
};
export const StartError = Template.bind({});
StartError.args = {
...Example.args,
agent: MockWorkspaceAgentStartError,
export const BunchOfApps: Story = {
args: {
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
workspace: MockWorkspace,
showApps: true,
},
};
export const ShuttingDown = Template.bind({});
ShuttingDown.args = {
...Example.args,
agent: MockWorkspaceAgentShuttingDown,
export const Connecting: Story = {
args: {
agent: MockWorkspaceAgentConnecting,
storybookAgentMetadata: [],
},
};
export const ShutdownTimeout = Template.bind({});
ShutdownTimeout.args = {
...Example.args,
agent: MockWorkspaceAgentShutdownTimeout,
export const Timeout: Story = {
args: {
agent: MockWorkspaceAgentTimeout,
},
};
export const ShutdownError = Template.bind({});
ShutdownError.args = {
...Example.args,
agent: MockWorkspaceAgentShutdownError,
export const Starting: Story = {
args: {
agent: MockWorkspaceAgentStarting,
},
};
export const Off = Template.bind({});
Off.args = {
...Example.args,
agent: MockWorkspaceAgentOff,
export const Started: Story = {
args: {
agent: {
...MockWorkspaceAgentReady,
logs_length: 1,
},
},
};
export const ShowingPortForward = TemplateWithPortForward.bind({});
ShowingPortForward.args = {
...Example.args,
export const StartedNoMetadata: Story = {
args: {
...Started.args,
storybookAgentMetadata: [],
},
};
export const Outdated = Template.bind({});
Outdated.args = {
...Example.args,
agent: MockWorkspaceAgentOutdated,
workspace: MockWorkspace,
serverVersion: "v99.999.9999+c1cdf14",
export const StartTimeout: Story = {
args: {
agent: MockWorkspaceAgentStartTimeout,
},
};
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,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows";
import {
LogLine,
logLineHeight,
} from "components/WorkspaceBuildLogs/Logs/Logs";
import { LogLine, logLineHeight } from "components/WorkspaceBuildLogs/Logs";
import { PortForwardButton } from "./PortForwardButton";
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
import {

View File

@ -1,44 +1,43 @@
import { Story } from "@storybook/react";
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",
component: AgentRowPreview,
};
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,
],
args: {
agent: MockWorkspaceAgent,
},
};
export const NoApps = Template.bind({});
NoApps.args = {
...Example.args,
agent: {
...MockWorkspaceAgent,
apps: [],
export default meta;
type Story = StoryObj<typeof AgentRowPreview>;
export const Example: Story = {};
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 {
MockPrimaryWorkspaceProxy,
MockWorkspaceProxies,
@ -7,116 +6,132 @@ import {
MockWorkspaceApp,
MockProxyLatencies,
} from "testHelpers/entities";
import { AppLink, AppLinkProps } from "./AppLink";
import { AppLink } from "./AppLink";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof AppLink> = {
title: "components/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) => (
<ProxyContext.Provider
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 default meta;
type Story = StoryObj<typeof AppLink>;
export const WithIcon = Template.bind({});
WithIcon.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
icon: "/icon/code.svg",
sharing_level: "owner",
health: "healthy",
export const WithIcon: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
icon: "/icon/code.svg",
sharing_level: "owner",
health: "healthy",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const ExternalApp = Template.bind({});
ExternalApp.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
external: true,
export const ExternalApp: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
external: true,
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const SharingLevelOwner = Template.bind({});
SharingLevelOwner.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "owner",
export const SharingLevelOwner: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "owner",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const SharingLevelAuthenticated = Template.bind({});
SharingLevelAuthenticated.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "authenticated",
export const SharingLevelAuthenticated: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "authenticated",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const SharingLevelPublic = Template.bind({});
SharingLevelPublic.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "public",
export const SharingLevelPublic: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "public",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const HealthDisabled = Template.bind({});
HealthDisabled.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "owner",
health: "disabled",
export const HealthDisabled: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
sharing_level: "owner",
health: "disabled",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const HealthInitializing = Template.bind({});
HealthInitializing.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
health: "initializing",
export const HealthInitializing: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
health: "initializing",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};
export const HealthUnhealthy = Template.bind({});
HealthUnhealthy.args = {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
health: "unhealthy",
export const HealthUnhealthy: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
health: "unhealthy",
},
agent: MockWorkspaceAgent,
},
agent: MockWorkspaceAgent,
};

View File

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

View File

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

View File

@ -1,124 +1,129 @@
import { action } from "@storybook/addon-actions";
import { Story } from "@storybook/react";
import {
MockProxyLatencies,
MockWorkspace,
MockWorkspaceResource,
} from "testHelpers/entities";
import { AgentRow } from "./AgentRow";
import { ResourceCard, ResourceCardProps } from "./ResourceCard";
import { ResourceCard } from "./ResourceCard";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof ResourceCard> = {
title: "components/ResourceCard",
component: ResourceCard,
};
const Template: Story<ResourceCardProps> = (args) => <ResourceCard {...args} />;
export const Example = Template.bind({});
Example.args = {
resource: MockWorkspaceResource,
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>
),
};
export const BunchOfMetadata = Template.bind({});
BunchOfMetadata.args = {
...Example.args,
resource: {
...MockWorkspaceResource,
metadata: [
{
key: "CPU(limits, requests)",
value: "2 cores, 500m",
sensitive: false,
},
{ key: "container image pull policy", value: "Always", sensitive: false },
{ key: "Disk", value: "10GiB", sensitive: false },
{
key: "image",
value: "docker.io/markmilligan/pycharm-community:latest",
sensitive: false,
},
{ key: "kubernetes namespace", value: "oss", sensitive: false },
{
key: "memory(limits, requests)",
value: "4GB, 500mi",
sensitive: false,
},
{
key: "security context - container",
value: "run_as_user 1000",
sensitive: false,
},
{
key: "security context - pod",
value: "run_as_user 1000 fs_group 1000",
sensitive: false,
},
{ key: "volume", value: "/home/coder", sensitive: false },
{
key: "secret",
value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI",
sensitive: true,
},
],
args: {
resource: MockWorkspaceResource,
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>
),
},
};
export default meta;
type Story = StoryObj<typeof ResourceCard>;
export const Example: Story = {};
export const BunchOfMetadata: Story = {
args: {
resource: {
...MockWorkspaceResource,
metadata: [
{
key: "CPU(limits, requests)",
value: "2 cores, 500m",
sensitive: false,
},
{
key: "container image pull policy",
value: "Always",
sensitive: false,
},
{ key: "Disk", value: "10GiB", sensitive: false },
{
key: "image",
value: "docker.io/markmilligan/pycharm-community:latest",
sensitive: false,
},
{ key: "kubernetes namespace", value: "oss", sensitive: false },
{
key: "memory(limits, requests)",
value: "4GB, 500mi",
sensitive: false,
},
{
key: "security context - container",
value: "run_as_user 1000",
sensitive: false,
},
{
key: "security context - pod",
value: "run_as_user 1000 fs_group 1000",
sensitive: false,
},
{ 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 { 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",
component: SSHButton,
};
const Template: Story<SSHButtonProps> = (args) => <SSHButton {...args} />;
export default meta;
type Story = StoryObj<typeof SSHButton>;
export const Closed = Template.bind({});
Closed.args = {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
sshPrefix: "coder.",
export const Closed: Story = {
args: {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
sshPrefix: "coder.",
},
};
export const Opened = Template.bind({});
Opened.args = {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
defaultIsOpen: true,
sshPrefix: "coder.",
export const Opened: Story = {
args: {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
defaultIsOpen: true,
sshPrefix: "coder.",
},
};

View File

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

View File

@ -1,29 +1,26 @@
import { Story } from "@storybook/react";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import {
VSCodeDesktopButton,
VSCodeDesktopButtonProps,
} from "./VSCodeDesktopButton";
import { VSCodeDesktopButton } from "./VSCodeDesktopButton";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof VSCodeDesktopButton> = {
title: "components/VSCodeDesktopButton",
component: VSCodeDesktopButton,
};
const Template: Story<VSCodeDesktopButtonProps> = (args) => (
<VSCodeDesktopButton {...args} />
);
export default meta;
type Story = StoryObj<typeof VSCodeDesktopButton>;
export const Default = Template.bind({});
Default.args = {
userName: MockWorkspace.owner_name,
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
displayApps: [
"vscode",
"port_forwarding_helper",
"ssh_helper",
"vscode_insiders",
"web_terminal",
],
export const Default: Story = {
args: {
userName: MockWorkspace.owner_name,
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
displayApps: [
"vscode",
"port_forwarding_helper",
"ssh_helper",
"vscode_insiders",
"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 { Stack } from "components/Stack/Stack";
import { FC } from "react";
import { TemplateVersionParameter } from "../../api/typesGenerated";
import { TemplateVersionParameter } from "api/typesGenerated";
import { colors } from "theme/colors";
import { MemoizedMarkdown } from "components/Markdown/Markdown";
import { MultiTextField } from "components/RichParameterInput/MultiTextField/MultiTextField";
import { MultiTextField } from "./MultiTextField";
import Box from "@mui/material/Box";
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, TableRowMenuProps } from "./TableRowMenu";
import { TableRowMenu } from "./TableRowMenu";
import type { Meta, StoryObj } from "@storybook/react";
export default {
const meta: Meta<typeof TableRowMenu> = {
title: "components/TableRowMenu",
component: TableRowMenu,
} as ComponentMeta<typeof TableRowMenu>;
type DataType = {
id: string;
};
const Template: Story<TableRowMenuProps<DataType>> = (args) => (
<TableRowMenu {...args} />
);
export default meta;
type Story = StoryObj<typeof TableRowMenu<{ id: string }>>;
export const Example = Template.bind({});
Example.args = {
data: { id: "123" },
menuItems: [
{ label: "Suspend", 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: "Explode", onClick: (data) => alert(data.id), disabled: true },
],
export const Example: Story = {
args: {
data: { id: "123" },
menuItems: [
{ label: "Suspend", 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: "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 {
TemplatePageHeader,
TemplatePageHeaderProps,
} from "./TemplatePageHeader";
import { TemplatePageHeader } from "./TemplatePageHeader";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Components/TemplatePageHeader",
const meta: Meta<typeof TemplatePageHeader> = {
title: "components/TemplatePageHeader",
component: TemplatePageHeader,
args: {
template: MockTemplate,
@ -15,18 +12,17 @@ export default {
canUpdateTemplate: true,
},
},
} as ComponentMeta<typeof TemplatePageHeader>;
};
const Template: Story<TemplatePageHeaderProps> = (args) => (
<TemplatePageHeader {...args} />
);
export default meta;
type Story = StoryObj<typeof TemplatePageHeader>;
export const CanUpdate = Template.bind({});
CanUpdate.args = {};
export const CanUpdate: Story = {};
export const CanNotUpdate = Template.bind({});
CanNotUpdate.args = {
permissions: {
canUpdateTemplate: false,
export const CanNotUpdate: Story = {
args: {
permissions: {
canUpdateTemplate: false,
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import * as API from "api/api";
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
// much more efficient. Instead of mapping objects each time, we're