diff --git a/site/.eslintrc.yaml b/site/.eslintrc.yaml index 5f8386529919a..d6c6be9eb6b95 100644 --- a/site/.eslintrc.yaml +++ b/site/.eslintrc.yaml @@ -92,8 +92,6 @@ rules: message: "Use path imports to avoid pulling in unused modules. See: https://material-ui.com/guides/minimizing-bundle-size/" - - name: "@material-ui/core/Tooltip" - message: "Use the custom Tooltip on componens/Tooltip" no-storage/no-browser-storage: error no-unused-vars: "off" "object-curly-spacing": "off" diff --git a/site/components/Button/CopyButton.tsx b/site/components/Button/CopyButton.tsx new file mode 100644 index 0000000000000..b10a6088da285 --- /dev/null +++ b/site/components/Button/CopyButton.tsx @@ -0,0 +1,68 @@ +import { makeStyles } from "@material-ui/core/styles" +import Button from "@material-ui/core/Button" +import Tooltip from "@material-ui/core/Tooltip" +import Check from "@material-ui/icons/Check" +import React, { useState } from "react" +import { FileCopy } from "../Icons" + +interface CopyButtonProps { + text: string + className?: string +} + +/** + * Copy button used inside the CodeBlock component internally + */ +export const CopyButton: React.FC = ({ className = "", text }) => { + const styles = useStyles() + const [isCopied, setIsCopied] = useState(false) + + const copyToClipboard = async (): Promise => { + try { + await window.navigator.clipboard.writeText(text) + setIsCopied(true) + + window.setTimeout(() => { + setIsCopied(false) + }, 1000) + } catch (err) { + const wrappedErr = new Error("copyToClipboard: failed to copy text to clipboard") + if (err instanceof Error) { + wrappedErr.stack = err.stack + } + console.error(wrappedErr) + } + } + + return ( + +
+ +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + copyButtonWrapper: { + display: "flex", + marginLeft: theme.spacing(1), + }, + copyButton: { + borderRadius: 7, + background: theme.palette.codeBlock.button.main, + color: theme.palette.codeBlock.button.contrastText, + padding: theme.spacing(0.85), + minWidth: 32, + + "&:hover": { + background: theme.palette.codeBlock.button.hover, + }, + }, + fileCopyIcon: { + width: 20, + height: 20, + }, +})) diff --git a/site/components/Button/index.ts b/site/components/Button/index.ts index 318c0c373932d..a0845e5d310f1 100644 --- a/site/components/Button/index.ts +++ b/site/components/Button/index.ts @@ -1,2 +1,3 @@ export * from "./SplitButton" export * from "./LoadingButton" +export * from "./CopyButton" diff --git a/site/components/CodeBlock/index.stories.tsx b/site/components/CodeBlock/index.stories.tsx index b31d450e21246..86e14ffca2856 100644 --- a/site/components/CodeBlock/index.stories.tsx +++ b/site/components/CodeBlock/index.stories.tsx @@ -12,7 +12,7 @@ export default { title: "CodeBlock", component: CodeBlock, argTypes: { - lines: { control: "object", defaultValue: sampleLines }, + lines: { control: "text", defaultValue: sampleLines }, }, } diff --git a/site/components/CodeBlock/index.tsx b/site/components/CodeBlock/index.tsx index 6b8b1afd336a1..e32468f705afc 100644 --- a/site/components/CodeBlock/index.tsx +++ b/site/components/CodeBlock/index.tsx @@ -1,5 +1,6 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" export interface CodeBlockProps { lines: string[] @@ -18,8 +19,6 @@ export const CodeBlock: React.FC = ({ lines }) => { ) } -const MONOSPACE_FONT_FAMILY = - "'Fira Code', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace" const useStyles = makeStyles((theme) => ({ root: { diff --git a/site/components/CodeExample/CodeExample.stories.tsx b/site/components/CodeExample/CodeExample.stories.tsx new file mode 100644 index 0000000000000..4eafb626a83d2 --- /dev/null +++ b/site/components/CodeExample/CodeExample.stories.tsx @@ -0,0 +1,20 @@ +import { Story } from "@storybook/react" +import React from "react" +import { CodeExample, CodeExampleProps } from "./CodeExample" + +const sampleCode = `echo "Hello, world"` + +export default { + title: "CodeExample", + component: CodeExample, + argTypes: { + code: { control: "string", defaultValue: sampleCode }, + }, +} + +const Template: Story = (args: CodeExampleProps) => + +export const Example = Template.bind({}) +Example.args = { + code: sampleCode, +} diff --git a/site/components/CodeExample/CodeExample.test.tsx b/site/components/CodeExample/CodeExample.test.tsx new file mode 100644 index 0000000000000..7618f29d38f34 --- /dev/null +++ b/site/components/CodeExample/CodeExample.test.tsx @@ -0,0 +1,15 @@ +import { screen } from "@testing-library/react" +import { render } from "../../test_helpers" +import React from "react" +import { CodeExample } from "./CodeExample" + +describe("CodeExample", () => { + it("renders code", async () => { + // When + render() + + // Then + // Both lines should be rendered + await screen.findByText("echo hello") + }) +}) diff --git a/site/components/CodeExample/CodeExample.tsx b/site/components/CodeExample/CodeExample.tsx new file mode 100644 index 0000000000000..c3145af8f349b --- /dev/null +++ b/site/components/CodeExample/CodeExample.tsx @@ -0,0 +1,38 @@ +import { makeStyles } from "@material-ui/core/styles" +import React from "react" +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" + +import { CopyButton } from "../Button" + +export interface CodeExampleProps { + code: string +} + +/** + * Component to show single-line code examples, with a copy button + */ +export const CodeExample: React.FC = ({ code }) => { + const styles = useStyles() + + return ( +
+ {code} + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + background: theme.palette.background.default, + color: theme.palette.codeBlock.contrastText, + fontFamily: MONOSPACE_FONT_FAMILY, + fontSize: 13, + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + }, +})) diff --git a/site/components/CodeExample/index.ts b/site/components/CodeExample/index.ts new file mode 100644 index 0000000000000..a2c38996d0f11 --- /dev/null +++ b/site/components/CodeExample/index.ts @@ -0,0 +1 @@ +export * from "./CodeExample" diff --git a/site/components/Icons/FileCopy.tsx b/site/components/Icons/FileCopy.tsx new file mode 100644 index 0000000000000..d8f33f101ef5f --- /dev/null +++ b/site/components/Icons/FileCopy.tsx @@ -0,0 +1,11 @@ +import SvgIcon from "@material-ui/core/SvgIcon" +import React from "react" + +export const FileCopy: typeof SvgIcon = (props) => ( + + + +) diff --git a/site/components/Icons/index.ts b/site/components/Icons/index.ts index 17f48ebaa4a98..bb6950f2f1da9 100644 --- a/site/components/Icons/index.ts +++ b/site/components/Icons/index.ts @@ -1,4 +1,5 @@ export { CoderIcon } from "./CoderIcon" +export * from "./FileCopy" export { Logo } from "./Logo" export * from "./Logout" export { WorkspacesIcon } from "./WorkspacesIcon" diff --git a/site/pages/projects/index.tsx b/site/pages/projects/index.tsx index bf67deb52502b..105216e069d39 100644 --- a/site/pages/projects/index.tsx +++ b/site/pages/projects/index.tsx @@ -1,7 +1,6 @@ import React from "react" import { makeStyles } from "@material-ui/core/styles" import Paper from "@material-ui/core/Paper" -import { useRouter } from "next/router" import Link from "next/link" import { EmptyState } from "../../components" import { ErrorSummary } from "../../components/ErrorSummary" @@ -14,10 +13,10 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Organization, Project } from "./../../api" import useSWR from "swr" +import { CodeExample } from "../../components/CodeExample/CodeExample" const ProjectsPage: React.FC = () => { const styles = useStyles() - const router = useRouter() const { me, signOut } = useUser(true) const { data: projects, error } = useSWR("/api/v2/projects") const { data: orgs, error: orgsError } = useSWR("/api/v2/users/me/organizations") @@ -34,15 +33,6 @@ const ProjectsPage: React.FC = () => { return } - const createProject = () => { - void router.push("/projects/create") - } - - const action = { - text: "Create Project", - onClick: createProject, - } - // Create a dictionary of organization ID -> organization Name // Needed to properly construct links to dive into a project const orgDictionary = orgs.reduce((acc: Record, curr: Organization) => { @@ -62,17 +52,15 @@ const ProjectsPage: React.FC = () => { }, ] - const emptyState = ( - + const description = ( +
+
Run the following command to get started:
+ +
) + const emptyState = + const tableProps = { title: "All Projects", columns: columns, @@ -85,7 +73,7 @@ const ProjectsPage: React.FC = () => { return (
-
+
@@ -94,11 +82,14 @@ const ProjectsPage: React.FC = () => { ) } -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme) => ({ root: { display: "flex", flexDirection: "column", }, + descriptionLabel: { + marginBottom: theme.spacing(1), + }, })) export default ProjectsPage diff --git a/site/theme/palettes.ts b/site/theme/palettes.ts index 82132d93f5b6b..312d1144ed32d 100644 --- a/site/theme/palettes.ts +++ b/site/theme/palettes.ts @@ -9,6 +9,14 @@ declare module "@material-ui/core/styles/createPalette" { contrastText: string // Background color for codeblocks main: string + button: { + // Background for buttons inside a codeblock + main: string + // Hover background color for buttons inside a codeblock + hover: string + // Text color for buttons inside a codeblock + contrastText: string + } } navbar: { main: string @@ -26,6 +34,11 @@ declare module "@material-ui/core/styles/createPalette" { codeBlock: { contrastText: string main: string + button: { + main: string + hover: string + contrastText: string + } } navbar: { main: string @@ -71,6 +84,11 @@ export const lightPalette: CustomPalette = { codeBlock: { main: "#F3F3F3", contrastText: "rgba(0, 0, 0, 0.9)", + button: { + main: "#E6ECE6", + hover: "#DAEBDA", + contrastText: "#000", + }, }, primary: { main: "#519A54", @@ -135,6 +153,11 @@ export const darkPalette: CustomPalette = { codeBlock: { main: "rgb(24, 26, 27)", contrastText: "rgba(255, 255, 255, 0.8)", + button: { + main: "rgba(255, 255, 255, 0.1)", + hover: "rgba(255, 255, 255, 0.25)", + contrastText: "#FFF", + }, }, hero: { main: "#141414",