-
Notifications
You must be signed in to change notification settings - Fork 582
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Initial Projects listing page (#58)
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
1 parent
69d88b4
commit b964cb0
Showing
13 changed files
with
602 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
Oops, something went wrong.