mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-09-01 00:59:07 +00:00
Rename frontend directory (#472)
* Rename directory * Cleanup * Recover lost images * remove lint
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Button } from "./Button";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: "Example/Button",
|
||||
component: Button,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: "color" },
|
||||
},
|
||||
} as ComponentMeta<typeof Button>;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: "large",
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
label: "Button",
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import "./button.css";
|
||||
|
||||
interface ButtonProps {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary?: boolean;
|
||||
/**
|
||||
* What background color to use
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* How large should the button be?
|
||||
*/
|
||||
size?: "small" | "medium" | "large";
|
||||
/**
|
||||
* Button contents
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button = ({
|
||||
primary = false,
|
||||
size = "medium",
|
||||
backgroundColor,
|
||||
label,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const mode = primary
|
||||
? "storybook-button--primary"
|
||||
: "storybook-button--secondary";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={["storybook-button", `storybook-button--${size}`, mode].join(
|
||||
" "
|
||||
)}
|
||||
style={{ backgroundColor }}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.storybook-button {
|
||||
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
.storybook-button--primary {
|
||||
color: white;
|
||||
background-color: #1ea7fd;
|
||||
}
|
||||
.storybook-button--secondary {
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
}
|
||||
.storybook-button--small {
|
||||
font-size: 12px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.storybook-button--medium {
|
||||
font-size: 14px;
|
||||
padding: 11px 20px;
|
||||
}
|
||||
.storybook-button--large {
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable no-console */
|
||||
// DropDown.stories.ts|tsx
|
||||
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import DropDown from "./DropDown";
|
||||
import { BsSlack, BsGithub } from "react-icons/bs";
|
||||
|
||||
//👇 This default export determines where your story goes in the story list
|
||||
export default {
|
||||
/* 👇 The title prop is optional.
|
||||
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
||||
* to learn how to generate automatic titles
|
||||
*/
|
||||
title: "DropDown",
|
||||
component: DropDown,
|
||||
} as ComponentMeta<typeof DropDown>;
|
||||
|
||||
//👇 We create a “template” of how args map to rendering
|
||||
const Template: ComponentStory<typeof DropDown> = (args) => (
|
||||
<DropDown {...args} />
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
const onClick = () => {
|
||||
console.log("drop down clicked");
|
||||
};
|
||||
|
||||
Default.args = {
|
||||
items: [
|
||||
{ id: "1", text: "Menu Item 1", onClick: onClick, icon: <BsSlack /> },
|
||||
{ id: "2 ", isSeparator: true },
|
||||
{ id: "3", text: "Menu Item 3", isDisabled: true, icon: <BsGithub /> },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import ArrowDownIcon from "../../assets/arrow-down-icon.svg";
|
||||
|
||||
export type DropDownItem = {
|
||||
id: string;
|
||||
text?: string;
|
||||
icon?: ReactNode;
|
||||
onClick?: () => void;
|
||||
isSeparator?: boolean;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type DropDownProps = {
|
||||
items: DropDownItem[];
|
||||
};
|
||||
|
||||
type PopupState = {
|
||||
isOpen: boolean;
|
||||
X: number;
|
||||
Y: number;
|
||||
};
|
||||
|
||||
function DropDown({ items }: DropDownProps) {
|
||||
const [popupState, setPopupState] = useState<PopupState>({
|
||||
isOpen: false,
|
||||
X: 0,
|
||||
Y: 0,
|
||||
});
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (popupState.isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [popupState.isOpen]);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
||||
setPopupState((prev) => ({
|
||||
...prev,
|
||||
isOpen: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-col items-center">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setPopupState((prev) => ({
|
||||
...prev,
|
||||
isOpen: !prev.isOpen,
|
||||
X: e.pageX,
|
||||
Y: e.pageY,
|
||||
}));
|
||||
}}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
Help
|
||||
<img src={ArrowDownIcon} className="ml-2 w-[10px] h-[10px]" />
|
||||
</button>
|
||||
</div>
|
||||
{popupState.isOpen && (
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`z-10 flex flex-col py-1 gap-1 bg-white mt-3 absolute rounded border top-[${popupState.Y}] left-[${popupState.X}] border-gray-200`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<>
|
||||
{item.isSeparator ? (
|
||||
<div className="bg-gray-300 h-[1px]" />
|
||||
) : (
|
||||
<div
|
||||
onClick={() => {
|
||||
item.onClick?.();
|
||||
setPopupState((prev) => ({
|
||||
...prev,
|
||||
isOpen: false,
|
||||
}));
|
||||
}}
|
||||
className={`cursor-pointer font-normal flex items-center gap-2 py-1 pl-3 pr-7 hover:bg-dropdown ${
|
||||
item.isDisabled
|
||||
? "cursor-default hover:bg-transparent text-gray-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{item.icon && <span> {item.icon ?? null}</span>}
|
||||
<span>{item.text}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Header } from "./Header";
|
||||
|
||||
export default {
|
||||
title: "Example/Header",
|
||||
component: Header,
|
||||
parameters: {
|
||||
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: "fullscreen",
|
||||
},
|
||||
} as ComponentMeta<typeof Header>;
|
||||
|
||||
const Template: ComponentStory<typeof Header> = (args) => <Header {...args} />;
|
||||
|
||||
export const LoggedIn = Template.bind({});
|
||||
LoggedIn.args = {
|
||||
user: {
|
||||
name: "Jane Doe",
|
||||
},
|
||||
};
|
||||
|
||||
export const LoggedOut = Template.bind({});
|
||||
LoggedOut.args = {};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Button } from "../Button/Button";
|
||||
import "./header.css";
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
user?: User;
|
||||
onLogin: () => void;
|
||||
onLogout: () => void;
|
||||
onCreateAccount: () => void;
|
||||
}
|
||||
|
||||
export const Header = ({
|
||||
user,
|
||||
onLogin,
|
||||
onLogout,
|
||||
onCreateAccount,
|
||||
}: HeaderProps) => (
|
||||
<header>
|
||||
<div className="wrapper">
|
||||
<div>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path
|
||||
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
|
||||
fill="#91BAF8"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
<span className="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button
|
||||
primary
|
||||
size="small"
|
||||
onClick={onCreateAccount}
|
||||
label="Sign up"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
.wrapper {
|
||||
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 900;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
margin: 6px 0 6px 10px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import { within, userEvent } from "@storybook/testing-library";
|
||||
import { Page } from "./Page";
|
||||
|
||||
export default {
|
||||
title: "Example/Page",
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: "fullscreen",
|
||||
},
|
||||
} as ComponentMeta<typeof Page>;
|
||||
|
||||
const Template: ComponentStory<typeof Page> = (args) => <Page {...args} />;
|
||||
|
||||
export const LoggedOut = Template.bind({});
|
||||
|
||||
export const LoggedIn = Template.bind({});
|
||||
|
||||
// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing
|
||||
LoggedIn.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = await canvas.getByRole("button", { name: /Log in/i });
|
||||
await userEvent.click(loginButton);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from "react";
|
||||
|
||||
import { Header } from "../Header/Header";
|
||||
import "./page.css";
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const Page: React.VFC = () => {
|
||||
const [user, setUser] = React.useState<User>();
|
||||
|
||||
return (
|
||||
<article>
|
||||
<Header
|
||||
user={user}
|
||||
onLogin={() => setUser({ name: "Jane Doe" })}
|
||||
onLogout={() => setUser(undefined)}
|
||||
onCreateAccount={() => setUser({ name: "Jane Doe" })}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a{" "}
|
||||
<a
|
||||
href="https://componentdriven.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>component-driven</strong>
|
||||
</a>{" "}
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review
|
||||
page states without needing to navigate to them in your app. Here are
|
||||
some handy patterns for managing page data in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose
|
||||
such data from the "args" of child component stories
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock
|
||||
these services out using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at{" "}
|
||||
<a
|
||||
href="https://storybook.js.org/tutorials/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the{" "}
|
||||
<a
|
||||
href="https://storybook.js.org/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="tip-wrapper">
|
||||
<span className="tip">Tip</span> Adjust the width of the canvas with
|
||||
the{" "}
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 12 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
section {
|
||||
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 48px 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-weight: 900;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
margin: 0 0 4px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
section a {
|
||||
text-decoration: none;
|
||||
color: #1ea7fd;
|
||||
}
|
||||
|
||||
section ul {
|
||||
padding-left: 30px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
section li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
section .tip {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
background: #e7fdd8;
|
||||
color: #66bf3c;
|
||||
padding: 4px 12px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
section .tip-wrapper {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section .tip-wrapper svg {
|
||||
display: inline-block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
section .tip-wrapper svg path {
|
||||
fill: #1ea7fd;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ComponentStory } from "@storybook/react";
|
||||
import StatusLabel, { DeploymentStatus } from "./StatusLabel";
|
||||
|
||||
export default {
|
||||
title: "StatusLabel",
|
||||
component: StatusLabel,
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof StatusLabel> = (args) => (
|
||||
<StatusLabel {...args} />
|
||||
);
|
||||
|
||||
export const Deployed = Template.bind({});
|
||||
|
||||
Deployed.args = {
|
||||
status: DeploymentStatus.DEPLOYED,
|
||||
isRollback: false,
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
|
||||
Failed.args = {
|
||||
status: DeploymentStatus.FAILED,
|
||||
isRollback: false,
|
||||
};
|
||||
|
||||
export const Pending = Template.bind({});
|
||||
|
||||
Pending.args = {
|
||||
status: DeploymentStatus.PENDING,
|
||||
isRollback: false,
|
||||
};
|
||||
|
||||
export const Superseded = Template.bind({});
|
||||
|
||||
Superseded.args = {
|
||||
status: DeploymentStatus.SUPERSEDED,
|
||||
isRollback: false,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AiOutlineReload } from "react-icons/ai";
|
||||
|
||||
type StatusLabelProps = {
|
||||
status: string;
|
||||
isRollback?: boolean;
|
||||
};
|
||||
|
||||
export enum DeploymentStatus {
|
||||
DEPLOYED = "deployed",
|
||||
FAILED = "failed",
|
||||
PENDING = "pending-install",
|
||||
SUPERSEDED = "superseded",
|
||||
}
|
||||
|
||||
export function getStatusColor(status: DeploymentStatus) {
|
||||
if (status === DeploymentStatus.DEPLOYED) return "text-deployed";
|
||||
if (status === DeploymentStatus.FAILED) return "text-failed";
|
||||
if (status === DeploymentStatus.PENDING) return "text-pending";
|
||||
else return "text-superseded";
|
||||
}
|
||||
|
||||
function StatusLabel({ status, isRollback }: StatusLabelProps) {
|
||||
const statusColor = getStatusColor(status as DeploymentStatus);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minWidth: "100px",
|
||||
display: "flex",
|
||||
fontSize: "14px",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span className={`${statusColor} font-bold text-xs`}>
|
||||
● {status.toUpperCase()}
|
||||
</span>
|
||||
{isRollback && <AiOutlineReload size={14} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusLabel;
|
||||
Reference in New Issue
Block a user