Skip to content

Commit

Permalink
feat: Initial Projects listing page (#58)
Browse files Browse the repository at this point in the history
This implements a simple Project listing page at `/projects` - just a table for a list of projects:

![image](https://user-images.githubusercontent.com/88213859/150906058-bbc49cfc-cb42-4252-bade-b8d48a986280.png)

...and an empty state:

![image](https://user-images.githubusercontent.com/88213859/150906882-03b0ace5-77c6-4806-b530-008769948867.png)

There isn't too much data to show at the moment. It'll be nice in the future to show the following fields and improve the UI with it:
- An icon
- A list of users using the project
- A description

However, this brings in a lot of scaffolding to make it easier to build pages like this (`/organizations`, `/workspaces`, etc).

In particular, I brought over a few things from v1:
- The `Hero` / `Header` component at the top of pages + sub-components
- A `Table` component for help rendering table-like UI + sub-components
- Additional palette settings that the `Hero`
  • Loading branch information
bryphe-coder committed Jan 25, 2022
1 parent 69d88b4 commit b964cb0
Show file tree
Hide file tree
Showing 13 changed files with 602 additions and 1 deletion.
11 changes: 11 additions & 0 deletions site/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ interface LoginResponse {
session_token: string
}

// This must be kept in sync with the `Project` struct in the back-end
export interface Project {
id: string
created_at: string
updated_at: string
organization_id: string
name: string
provisioner: string
active_version_id: string
}

export const login = async (email: string, password: string): Promise<LoginResponse> => {
const response = await fetch("/api/v2/login", {
method: "POST",
Expand Down
15 changes: 15 additions & 0 deletions site/components/ErrorSummary/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { render, screen } from "@testing-library/react"
import React from "react"
import { ErrorSummary } from "./index"

describe("ErrorSummary", () => {
it("renders", async () => {
// When
const error = new Error("test error message")
render(<ErrorSummary error={error} />)

// Then
const element = await screen.findByText("test error message", { exact: false })
expect(element).toBeDefined()
})
})
10 changes: 10 additions & 0 deletions site/components/ErrorSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from "react"

export interface ErrorSummaryProps {
error: Error
}

export const ErrorSummary: React.FC<ErrorSummaryProps> = ({ error }) => {
// TODO: More interesting error page
return <div>{error.toString()}</div>
}
37 changes: 37 additions & 0 deletions site/components/Header/HeaderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Button from "@material-ui/core/Button"
import { lighten, makeStyles } from "@material-ui/core/styles"
import React from "react"

export interface HeaderButtonProps {
readonly text: string
readonly disabled?: boolean
readonly onClick?: (event: MouseEvent) => void
}

export const HeaderButton: React.FC<HeaderButtonProps> = (props) => {
const styles = useStyles()

return (
<Button
className={styles.pageButton}
variant="contained"
onClick={(event: React.MouseEvent): void => {
if (props.onClick) {
props.onClick(event.nativeEvent)
}
}}
disabled={props.disabled}
component="button"
>
{props.text}
</Button>
)
}

const useStyles = makeStyles((theme) => ({
pageButton: {
whiteSpace: "nowrap",
backgroundColor: lighten(theme.palette.hero.main, 0.1),
color: "#B5BFD2",
},
}))
28 changes: 28 additions & 0 deletions site/components/Header/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { screen } from "@testing-library/react"
import { render } from "./../../test_helpers"
import React from "react"
import { Header } from "./index"

describe("Header", () => {
it("renders title and subtitle", async () => {
// When
render(<Header title="Title Test" subTitle="Subtitle Test" />)

// Then
const titleElement = await screen.findByText("Title Test")
expect(titleElement).toBeDefined()

const subTitleElement = await screen.findByText("Subtitle Test")
expect(subTitleElement).toBeDefined()
})

it("renders button if specified", async () => {
// When
render(<Header title="Title" action={{ text: "Button Test" }} />)

// Then
const buttonElement = await screen.findByRole("button")
expect(buttonElement).toBeDefined()
expect(buttonElement.textContent).toEqual("Button Test")
})
})
116 changes: 116 additions & 0 deletions site/components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import Box from "@material-ui/core/Box"
import Typography from "@material-ui/core/Typography"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { HeaderButton } from "./HeaderButton"

export interface HeaderAction {
readonly text: string
readonly onClick?: (event: MouseEvent) => void
}

export interface HeaderProps {
description?: string
title: string
subTitle?: string
action?: HeaderAction
}

export const Header: React.FC<HeaderProps> = ({ description, title, subTitle, action }) => {
const styles = useStyles()

return (
<div className={styles.root}>
<div className={styles.top}>
<div className={styles.topInner}>
<Box display="flex" flexDirection="column" minWidth={0}>
<div>
<Box display="flex" alignItems="center">
<Typography variant="h3" className={styles.title}>
<Box component="span" maxWidth="100%" overflow="hidden" textOverflow="ellipsis">
{title}
</Box>
</Typography>

{subTitle && (
<div className={styles.subtitle}>
<Typography style={{ fontSize: 16 }}>{subTitle}</Typography>
</div>
)}
</Box>
{description && (
<Typography variant="caption" className={styles.description}>
{description}
</Typography>
)}
</div>
</Box>

{action && (
<>
<div className={styles.actions}>
<HeaderButton key={action.text} {...action} />
</div>
</>
)}
</div>
</div>
</div>
)
}

const secondaryText = "#B5BFD2"
const useStyles = makeStyles((theme) => ({
root: {},
top: {
position: "relative",
display: "flex",
alignItems: "center",
height: 150,
background: theme.palette.hero.main,
boxShadow: theme.shadows[3],
},
topInner: {
display: "flex",
alignItems: "center",
maxWidth: "1380px",
margin: "0 auto",
flex: 1,
height: 68,
minWidth: 0,
},
title: {
display: "flex",
alignItems: "center",
fontWeight: "bold",
whiteSpace: "nowrap",
minWidth: 0,
color: theme.palette.primary.contrastText,
},
description: {
display: "block",
marginTop: theme.spacing(1) / 2,
marginBottom: -26,
color: secondaryText,
},
subtitle: {
position: "relative",
top: 2,
display: "flex",
alignItems: "center",
borderLeft: `1px solid ${theme.palette.divider}`,
height: 28,
marginLeft: 16,
paddingLeft: 16,
color: secondaryText,
},
actions: {
paddingLeft: "50px",
paddingRight: 0,
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
},
}))
82 changes: 82 additions & 0 deletions site/components/Table/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { screen } from "@testing-library/react"
import { render } from "./../../test_helpers"
import React from "react"
import { Table, Column } from "./Table"

interface TestData {
name: string
description: string
}

const columns: Column<TestData>[] = [
{
name: "Name",
key: "name",
},
{
name: "Description",
key: "description",
// For description, we'll test out the custom renderer path
renderer: (field) => <span>{"!!" + field + "!!"}</span>,
},
]

const data: TestData[] = [{ name: "AName", description: "ADescription" }]
const emptyData: TestData[] = []

describe("Table", () => {
it("renders empty state if empty", async () => {
// Given
const emptyState = <div>Empty Table!</div>
const tableProps = {
title: "TitleTest",
data: emptyData,
columns,
emptyState,
}

// When
render(<Table {...tableProps} />)

// Then
// Since there are no items, our empty state should've rendered
const emptyTextElement = await screen.findByText("Empty Table!")
expect(emptyTextElement).toBeDefined()
})

it("renders title", async () => {
// Given
const tableProps = {
title: "TitleTest",
data: emptyData,
columns,
}

// When
render(<Table {...tableProps} />)

// Then
const titleElement = await screen.findByText("TitleTest")
expect(titleElement).toBeDefined()
})

it("renders data fields with default renderer if none provided", async () => {
// Given
const tableProps = {
title: "TitleTest",
data,
columns,
}

// When
render(<Table {...tableProps} />)

// Then
// Check that the 'name' was rendered, with the default renderer
const nameElement = await screen.findByText("AName")
expect(nameElement).toBeDefined()
// ...and the description used our custom rendered
const descriptionElement = await screen.findByText("!!ADescription!!")
expect(descriptionElement).toBeDefined()
})
})

0 comments on commit b964cb0

Please sign in to comment.