mirror of
https://github.com/coder/coder.git
synced 2025-06-28 04:33:02 +00:00
fix: Remove 'Create Project' button, replace with CLI prompt (#245)
For black-triangle and alpha builds, we won't be able to create projects in the UI, because they require collecting and tar'ing a set of assets associated with the project - so the CLI is going to be our entry point for creating projects. This shifts the UI to remove the 'Create Project' button, and adds a prompt to copy a command to run. __Before:__ <img width="1134" alt="image" src="https://user-images.githubusercontent.com/88213859/153534269-58dc95bd-0417-4bed-8e62-e2b6f479da61.png"> __After:__ 
This commit is contained in:
@ -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"
|
||||
|
68
site/components/Button/CopyButton.tsx
Normal file
68
site/components/Button/CopyButton.tsx
Normal file
@ -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<CopyButtonProps> = ({ className = "", text }) => {
|
||||
const styles = useStyles()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
|
||||
const copyToClipboard = async (): Promise<void> => {
|
||||
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 (
|
||||
<Tooltip title="Copy to Clipboard" placement="top">
|
||||
<div className={`${styles.copyButtonWrapper} ${className}`}>
|
||||
<Button className={styles.copyButton} onClick={copyToClipboard} size="small">
|
||||
{isCopied ? <Check className={styles.fileCopyIcon} /> : <FileCopy className={styles.fileCopyIcon} />}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}))
|
@ -1,2 +1,3 @@
|
||||
export * from "./SplitButton"
|
||||
export * from "./LoadingButton"
|
||||
export * from "./CopyButton"
|
||||
|
@ -12,7 +12,7 @@ export default {
|
||||
title: "CodeBlock",
|
||||
component: CodeBlock,
|
||||
argTypes: {
|
||||
lines: { control: "object", defaultValue: sampleLines },
|
||||
lines: { control: "text", defaultValue: sampleLines },
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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<CodeBlockProps> = ({ lines }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const MONOSPACE_FONT_FAMILY =
|
||||
"'Fira Code', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace"
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
|
20
site/components/CodeExample/CodeExample.stories.tsx
Normal file
20
site/components/CodeExample/CodeExample.stories.tsx
Normal file
@ -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<CodeExampleProps> = (args: CodeExampleProps) => <CodeExample {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
code: sampleCode,
|
||||
}
|
15
site/components/CodeExample/CodeExample.test.tsx
Normal file
15
site/components/CodeExample/CodeExample.test.tsx
Normal file
@ -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(<CodeExample code="echo hello" />)
|
||||
|
||||
// Then
|
||||
// Both lines should be rendered
|
||||
await screen.findByText("echo hello")
|
||||
})
|
||||
})
|
38
site/components/CodeExample/CodeExample.tsx
Normal file
38
site/components/CodeExample/CodeExample.tsx
Normal file
@ -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<CodeExampleProps> = ({ code }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<code>{code}</code>
|
||||
<CopyButton text={code} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}))
|
1
site/components/CodeExample/index.ts
Normal file
1
site/components/CodeExample/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./CodeExample"
|
11
site/components/Icons/FileCopy.tsx
Normal file
11
site/components/Icons/FileCopy.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import SvgIcon from "@material-ui/core/SvgIcon"
|
||||
import React from "react"
|
||||
|
||||
export const FileCopy: typeof SvgIcon = (props) => (
|
||||
<SvgIcon {...props} viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M12.7412 2.2807H4.32014C3.5447 2.2807 2.91663 2.90877 2.91663 3.68421V13.5088H4.32014V3.68421H12.7412V2.2807ZM14.8465 5.08772H7.12716C6.35172 5.08772 5.72365 5.71579 5.72365 6.49123V16.3158C5.72365 17.0912 6.35172 17.7193 7.12716 17.7193H14.8465C15.6219 17.7193 16.25 17.0912 16.25 16.3158V6.49123C16.25 5.71579 15.6219 5.08772 14.8465 5.08772ZM14.8465 16.3158H7.12716V6.49123H14.8465V16.3158Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
)
|
@ -1,4 +1,5 @@
|
||||
export { CoderIcon } from "./CoderIcon"
|
||||
export * from "./FileCopy"
|
||||
export { Logo } from "./Logo"
|
||||
export * from "./Logout"
|
||||
export { WorkspacesIcon } from "./WorkspacesIcon"
|
||||
|
@ -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<Project[] | null, Error>("/api/v2/projects")
|
||||
const { data: orgs, error: orgsError } = useSWR<Organization[], Error>("/api/v2/users/me/organizations")
|
||||
@ -34,15 +33,6 @@ const ProjectsPage: React.FC = () => {
|
||||
return <FullScreenLoader />
|
||||
}
|
||||
|
||||
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<string, string>, curr: Organization) => {
|
||||
@ -62,17 +52,15 @@ const ProjectsPage: React.FC = () => {
|
||||
},
|
||||
]
|
||||
|
||||
const emptyState = (
|
||||
<EmptyState
|
||||
button={{
|
||||
children: "Create Project",
|
||||
onClick: createProject,
|
||||
}}
|
||||
message="No projects have been created yet"
|
||||
description="Create a project to get started."
|
||||
/>
|
||||
const description = (
|
||||
<div>
|
||||
<div className={styles.descriptionLabel}>Run the following command to get started:</div>
|
||||
<CodeExample code="coder project create" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const emptyState = <EmptyState message="No projects have been created yet" description={description} />
|
||||
|
||||
const tableProps = {
|
||||
title: "All Projects",
|
||||
columns: columns,
|
||||
@ -85,7 +73,7 @@ const ProjectsPage: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Navbar user={me} onSignOut={signOut} />
|
||||
<Header title="Projects" subTitle={subTitle} action={action} />
|
||||
<Header title="Projects" subTitle={subTitle} />
|
||||
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>
|
||||
<Table {...tableProps} />
|
||||
</Paper>
|
||||
@ -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
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user