chore: cleaning up workspaces table code (#2765)

* cleaning up workspace table code

* Update site/src/components/WorkspacesTable/WorkspacesTableBody.tsx

Co-authored-by: Joe Previte <jjprevite@gmail.com>

Co-authored-by: Joe Previte <jjprevite@gmail.com>
This commit is contained in:
Kira Pilot
2022-06-30 16:50:15 -04:00
committed by GitHub
co-authored by Joe Previte
parent 29be359f3d
commit ae59f166fd
12 changed files with 310 additions and 235 deletions
@@ -0,0 +1 @@
export * from "./HelpTooltip"
@@ -0,0 +1,33 @@
import RefreshIcon from "@material-ui/icons/Refresh"
import { FC } from "react"
import {
HelpTooltip,
HelpTooltipAction,
HelpTooltipLinksGroup,
HelpTooltipText,
HelpTooltipTitle,
} from "./HelpTooltip"
const Language = {
outdatedLabel: "Outdated",
versionTooltipText: "This workspace version is outdated and a newer version is available.",
updateVersionLabel: "Update version",
}
interface TooltipProps {
onUpdateVersion: () => void
}
export const OutdatedHelpTooltip: FC<TooltipProps> = ({ onUpdateVersion }) => {
return (
<HelpTooltip size="small">
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion}>
{Language.updateVersionLabel}
</HelpTooltipAction>
</HelpTooltipLinksGroup>
</HelpTooltip>
)
}
@@ -0,0 +1,37 @@
import { FC } from "react"
import {
HelpTooltip,
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
HelpTooltipTitle,
} from "./HelpTooltip"
const Language = {
workspaceTooltipTitle: "What is a workspace?",
workspaceTooltipText:
"A workspace is your development environment in the cloud. It includes the infrastructure and tools you need to work on your project.",
workspaceTooltipLink1: "Create workspaces",
workspaceTooltipLink2: "Connect with SSH",
workspaceTooltipLink3: "Editors and IDEs",
}
export const WorkspaceHelpTooltip: FC = () => {
return (
<HelpTooltip>
<HelpTooltipTitle>{Language.workspaceTooltipTitle}</HelpTooltipTitle>
<HelpTooltipText>{Language.workspaceTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#create-workspaces">
{Language.workspaceTooltipLink1}
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#connect-with-ssh">
{Language.workspaceTooltipLink2}
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#editors-and-ides">
{Language.workspaceTooltipLink3}
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</HelpTooltip>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { AgentHelpTooltip } from "./AgentHelpTooltip"
export { OutdatedHelpTooltip } from "./OutdatedHelpTooltip"
export { ResourcesHelpTooltip } from "./ResourcesHelpTooltip"
export { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"
@@ -47,7 +47,7 @@ export const UsersTableBody: FC<UsersTableBodyProps> = ({
return <TableLoader />
}
if (!users || users.length === 0) {
if (!users || !users.length) {
return (
<TableRow>
<TableCell colSpan={999}>
@@ -0,0 +1,107 @@
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
import TableRow from "@material-ui/core/TableRow"
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
import useTheme from "@material-ui/styles/useTheme"
import { useActor } from "@xstate/react"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { FC } from "react"
import { useNavigate } from "react-router-dom"
import { getDisplayStatus } from "../../util/workspace"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
import { AvatarData } from "../AvatarData/AvatarData"
import { TableCellLink } from "../TableCellLink/TableCellLink"
import { OutdatedHelpTooltip } from "../Tooltips"
dayjs.extend(relativeTime)
const Language = {
upToDateLabel: "Up to date",
outdatedLabel: "Outdated",
}
export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ workspaceRef }) => {
const styles = useStyles()
const navigate = useNavigate()
const theme: Theme = useTheme()
const [workspaceState, send] = useActor(workspaceRef)
const { data: workspace } = workspaceState.context
const status = getDisplayStatus(theme, workspace.latest_build)
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
return (
<TableRow
hover
data-testid={`workspace-${workspace.id}`}
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(workspacePageLink)
}
}}
className={styles.clickableTableRow}
>
<TableCellLink to={workspacePageLink}>
<AvatarData title={workspace.name} subtitle={workspace.owner_name} />
</TableCellLink>
<TableCellLink to={workspacePageLink}>{workspace.template_name}</TableCellLink>
<TableCellLink to={workspacePageLink}>
{workspace.outdated ? (
<span className={styles.outdatedLabel}>
{Language.outdatedLabel}
<OutdatedHelpTooltip
onUpdateVersion={() => {
send("UPDATE_VERSION")
}}
/>
</span>
) : (
<span style={{ color: theme.palette.text.secondary }}>{Language.upToDateLabel}</span>
)}
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<span data-chromatic="ignore" style={{ color: theme.palette.text.secondary }}>
{dayjs().to(dayjs(workspace.latest_build.created_at))}
</span>
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<span style={{ color: status.color }}>{status.status}</span>
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} />
</div>
</TableCellLink>
</TableRow>
)
}
const useStyles = makeStyles((theme) => ({
clickableTableRow: {
"&:hover td": {
backgroundColor: fade(theme.palette.primary.light, 0.1),
},
"&:focus": {
outline: `1px solid ${theme.palette.secondary.dark}`,
},
"& .MuiTableCell-root:last-child": {
paddingRight: theme.spacing(2),
},
},
arrowRight: {
color: fade(theme.palette.primary.contrastText, 0.7),
width: 20,
height: 20,
},
arrowCell: {
display: "flex",
},
outdatedLabel: {
color: theme.palette.error.main,
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
},
}))
@@ -0,0 +1,42 @@
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { FC } from "react"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
import { WorkspacesTableBody } from "./WorkspacesTableBody"
const Language = {
name: "Name",
template: "Template",
version: "Version",
lastBuilt: "Last Built",
status: "Status",
}
export interface WorkspacesTableProps {
isLoading?: boolean
workspaceRefs?: WorkspaceItemMachineRef[]
filter?: string
}
export const WorkspacesTable: FC<WorkspacesTableProps> = ({ isLoading, workspaceRefs, filter }) => {
return (
<Table>
<TableHead>
<TableRow>
<TableCell width="35%">{Language.name}</TableCell>
<TableCell width="15%">{Language.template}</TableCell>
<TableCell width="15%">{Language.version}</TableCell>
<TableCell width="20%">{Language.lastBuilt}</TableCell>
<TableCell width="15%">{Language.status}</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
<WorkspacesTableBody isLoading={isLoading} workspaceRefs={workspaceRefs} filter={filter} />
</TableBody>
</Table>
)
}
@@ -0,0 +1,69 @@
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"
import TableCell from "@material-ui/core/TableCell"
import TableRow from "@material-ui/core/TableRow"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import { FC } from "react"
import { Link as RouterLink } from "react-router-dom"
import { workspaceFilterQuery } from "../../util/filters"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
import { EmptyState } from "../EmptyState/EmptyState"
import { TableLoader } from "../TableLoader/TableLoader"
import { WorkspacesRow } from "./WorkspacesRow"
export const Language = {
emptyCreateWorkspaceMessage: "Create your first workspace",
emptyCreateWorkspaceDescription: "Start editing your source code and building your software.",
createFromTemplateButton: "Create from template",
emptyResultsMessage: "No results matched your search",
}
interface TableBodyProps {
isLoading?: boolean
workspaceRefs?: WorkspaceItemMachineRef[]
filter?: string
}
export const WorkspacesTableBody: FC<TableBodyProps> = ({ isLoading, workspaceRefs, filter }) => {
if (isLoading) {
return <TableLoader />
}
if (!workspaceRefs || !workspaceRefs.length) {
return (
<>
{filter === workspaceFilterQuery.me || filter === workspaceFilterQuery.all ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message={Language.emptyCreateWorkspaceMessage}
description={Language.emptyCreateWorkspaceDescription}
cta={
<Link underline="none" component={RouterLink} to="/templates">
<Button startIcon={<AddCircleOutline />}>
{Language.createFromTemplateButton}
</Button>
</Link>
}
/>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message={Language.emptyResultsMessage} />
</TableCell>
</TableRow>
)}
</>
)
}
return (
<>
{workspaceRefs.map((workspaceRef) => (
<WorkspacesRow workspaceRef={workspaceRef} key={workspaceRef.id} />
))}
</>
)
}
+1 -1
View File
@@ -64,7 +64,7 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
) : undefined
}
>
<PageHeaderTitle>Users</PageHeaderTitle>
<PageHeaderTitle>{Language.pageTitle}</PageHeaderTitle>
</PageHeader>
<SearchBarWithFilter
@@ -1,10 +1,10 @@
import { screen } from "@testing-library/react"
import { rest } from "msw"
import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody"
import { MockWorkspace } from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import WorkspacesPage from "./WorkspacesPage"
import { Language } from "./WorkspacesPageView"
describe("WorkspacesPage", () => {
beforeEach(() => {
@@ -23,7 +23,7 @@ describe("WorkspacesPage", () => {
render(<WorkspacesPage />)
// Then
await screen.findByText(Language.emptyCreateWorkspaceMessage)
await screen.findByText(WorkspacesTableBodyLanguage.emptyCreateWorkspaceMessage)
})
it("renders a filled workspaces page", async () => {
@@ -32,7 +32,7 @@ const WorkspacesPage: FC = () => {
<WorkspacesPageView
filter={workspacesState.context.filter}
loading={workspacesState.hasTag("loading")}
isLoading={workspacesState.hasTag("loading")}
workspaceRefs={workspaceRefs}
onFilter={(query) => {
setSearchParams({ filter: query })
@@ -1,22 +1,6 @@
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
import RefreshIcon from "@material-ui/icons/Refresh"
import useTheme from "@material-ui/styles/useTheme"
import { useActor } from "@xstate/react"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { FC } from "react"
import { Link as RouterLink, useNavigate } from "react-router-dom"
import { AvatarData } from "../../components/AvatarData/AvatarData"
import { EmptyState } from "../../components/EmptyState/EmptyState"
import { Link as RouterLink } from "react-router-dom"
import { Margins } from "../../components/Margins/Margins"
import {
PageHeader,
@@ -25,140 +9,28 @@ import {
} from "../../components/PageHeader/PageHeader"
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
import { Stack } from "../../components/Stack/Stack"
import { TableCellLink } from "../../components/TableCellLink/TableCellLink"
import { TableLoader } from "../../components/TableLoader/TableLoader"
import {
HelpTooltip,
HelpTooltipAction,
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
HelpTooltipTitle,
} from "../../components/Tooltips/HelpTooltip/HelpTooltip"
import { WorkspaceHelpTooltip } from "../../components/Tooltips"
import { WorkspacesTable } from "../../components/WorkspacesTable/WorkspacesTable"
import { workspaceFilterQuery } from "../../util/filters"
import { getDisplayStatus } from "../../util/workspace"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
dayjs.extend(relativeTime)
export const Language = {
createFromTemplateButton: "Create from template",
emptyCreateWorkspaceMessage: "Create your first workspace",
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
emptyResultsMessage: "No results matched your search",
pageTitle: "Workspaces",
yourWorkspacesButton: "Your workspaces",
allWorkspacesButton: "All workspaces",
workspaceTooltipTitle: "What is a workspace?",
workspaceTooltipText:
"A workspace is your development environment in the cloud. It includes the infrastructure and tools you need to work on your project.",
workspaceTooltipLink1: "Create workspaces",
workspaceTooltipLink2: "Connect with SSH",
workspaceTooltipLink3: "Editors and IDEs",
outdatedLabel: "Outdated",
upToDateLabel: "Up to date",
versionTooltipText: "This workspace version is outdated and a newer version is available.",
updateVersionLabel: "Update version",
}
const WorkspaceHelpTooltip: React.FC = () => {
return (
<HelpTooltip>
<HelpTooltipTitle>{Language.workspaceTooltipTitle}</HelpTooltipTitle>
<HelpTooltipText>{Language.workspaceTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#create-workspaces">
{Language.workspaceTooltipLink1}
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#connect-with-ssh">
{Language.workspaceTooltipLink2}
</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#editors-and-ides">
{Language.workspaceTooltipLink3}
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</HelpTooltip>
)
}
const OutdatedHelpTooltip: React.FC<{ onUpdateVersion: () => void }> = ({ onUpdateVersion }) => {
return (
<HelpTooltip size="small">
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion}>
{Language.updateVersionLabel}
</HelpTooltipAction>
</HelpTooltipLinksGroup>
</HelpTooltip>
)
}
const WorkspaceRow: React.FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ workspaceRef }) => {
const styles = useStyles()
const navigate = useNavigate()
const theme: Theme = useTheme()
const [workspaceState, send] = useActor(workspaceRef)
const { data: workspace } = workspaceState.context
const status = getDisplayStatus(theme, workspace.latest_build)
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
return (
<TableRow
hover
data-testid={`workspace-${workspace.id}`}
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(workspacePageLink)
}
}}
className={styles.clickableTableRow}
>
<TableCellLink to={workspacePageLink}>
<AvatarData title={workspace.name} subtitle={workspace.owner_name} />
</TableCellLink>
<TableCellLink to={workspacePageLink}>{workspace.template_name}</TableCellLink>
<TableCellLink to={workspacePageLink}>
{workspace.outdated ? (
<span className={styles.outdatedLabel}>
{Language.outdatedLabel}
<OutdatedHelpTooltip
onUpdateVersion={() => {
send("UPDATE_VERSION")
}}
/>
</span>
) : (
<span style={{ color: theme.palette.text.secondary }}>{Language.upToDateLabel}</span>
)}
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<span data-chromatic="ignore" style={{ color: theme.palette.text.secondary }}>
{dayjs().to(dayjs(workspace.latest_build.created_at))}
</span>
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<span style={{ color: status.color }}>{status.status}</span>
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} />
</div>
</TableCellLink>
</TableRow>
)
createANewWorkspace: `Create a new workspace from a `,
template: "Template",
}
export interface WorkspacesPageViewProps {
loading?: boolean
isLoading?: boolean
workspaceRefs?: WorkspaceItemMachineRef[]
filter?: string
onFilter: (query: string) => void
}
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
loading,
isLoading,
workspaceRefs,
filter,
onFilter,
@@ -173,15 +45,15 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
<PageHeader>
<PageHeaderTitle>
<Stack direction="row" spacing={1} alignItems="center">
<span>Workspaces</span>
<span>{Language.pageTitle}</span>
<WorkspaceHelpTooltip />
</Stack>
</PageHeaderTitle>
<PageHeaderSubtitle>
Create a new workspace from a{" "}
{Language.createANewWorkspace}
<Link component={RouterLink} to="/templates">
Template
{Language.template}
</Link>
.
</PageHeaderSubtitle>
@@ -189,97 +61,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
<SearchBarWithFilter filter={filter} onFilter={onFilter} presetFilters={presetFilters} />
<Table>
<TableHead>
<TableRow>
<TableCell width="35%">Name</TableCell>
<TableCell width="15%">Template</TableCell>
<TableCell width="15%">Version</TableCell>
<TableCell width="20%">Last Built</TableCell>
<TableCell width="15%">Status</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{!workspaceRefs && loading && <TableLoader />}
{workspaceRefs && workspaceRefs.length === 0 && (
<>
{filter === workspaceFilterQuery.me || filter === workspaceFilterQuery.all ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message={Language.emptyCreateWorkspaceMessage}
description={Language.emptyCreateWorkspaceDescription}
cta={
<Link underline="none" component={RouterLink} to="/templates">
<Button startIcon={<AddCircleOutline />}>
{Language.createFromTemplateButton}
</Button>
</Link>
}
/>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message={Language.emptyResultsMessage} />
</TableCell>
</TableRow>
)}
</>
)}
{workspaceRefs &&
workspaceRefs.map((workspaceRef) => (
<WorkspaceRow workspaceRef={workspaceRef} key={workspaceRef.id} />
))}
</TableBody>
</Table>
<WorkspacesTable isLoading={isLoading} workspaceRefs={workspaceRefs} filter={filter} />
</Margins>
)
}
const useStyles = makeStyles((theme) => ({
welcome: {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(12),
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
"& span": {
maxWidth: 600,
textAlign: "center",
fontSize: theme.spacing(2),
lineHeight: `${theme.spacing(3)}px`,
},
},
clickableTableRow: {
"&:hover td": {
backgroundColor: fade(theme.palette.primary.light, 0.1),
},
"&:focus": {
outline: `1px solid ${theme.palette.secondary.dark}`,
},
"& .MuiTableCell-root:last-child": {
paddingRight: theme.spacing(2),
},
},
arrowRight: {
color: fade(theme.palette.primary.contrastText, 0.7),
width: 20,
height: 20,
},
arrowCell: {
display: "flex",
},
outdatedLabel: {
color: theme.palette.error.main,
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
},
}))