refactor(site): simplify workspaces page component structure (#8793)

This commit is contained in:
Bruno Quaresma
2023-07-28 13:07:50 -03:00
committed by GitHub
parent bd944e0d21
commit a75346dd32
7 changed files with 281 additions and 328 deletions
+1 -10
View File
@@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles"
import TableCell from "@mui/material/TableCell"
import TableRow from "@mui/material/TableRow"
import { FC } from "react"
@@ -10,19 +9,11 @@ import {
export type TableEmptyProps = EmptyStateProps
export const TableEmpty: FC<TableEmptyProps> = (props) => {
const styles = useStyles()
return (
<TableRow>
<TableCell colSpan={999} className={styles.tableCell}>
<TableCell colSpan={999} sx={{ padding: "0 !important" }}>
<EmptyState {...props} />
</TableCell>
</TableRow>
)
}
const useStyles = makeStyles(() => ({
tableCell: {
padding: "0 !important",
},
}))
@@ -1,132 +0,0 @@
import TableCell from "@mui/material/TableCell"
import { makeStyles } from "@mui/styles"
import TableRow from "@mui/material/TableRow"
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
import { AvatarData } from "components/AvatarData/AvatarData"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { FC } from "react"
import { useNavigate } from "react-router-dom"
import { getDisplayWorkspaceTemplateName } from "utils/workspace"
import { LastUsed } from "../LastUsed/LastUsed"
import { Workspace } from "api/typesGenerated"
import { WorkspaceOutdatedTooltip } from "components/Tooltips/WorkspaceOutdatedTooltip"
import { Avatar } from "components/Avatar/Avatar"
import { Stack } from "components/Stack/Stack"
import { useClickableTableRow } from "hooks/useClickableTableRow"
import {
HelpTooltip,
HelpTooltipText,
HelpTooltipTitle,
} from "components/Tooltips/HelpTooltip"
import InfoIcon from "@mui/icons-material/InfoOutlined"
import { colors } from "theme/colors"
import Box from "@mui/material/Box"
export const WorkspacesRow: FC<{
workspace: Workspace
onUpdateWorkspace: (workspace: Workspace) => void
}> = ({ workspace, onUpdateWorkspace }) => {
const styles = useStyles()
const navigate = useNavigate()
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace)
const clickable = useClickableTableRow(() => {
navigate(workspacePageLink)
})
return (
<TableRow data-testid={`workspace-${workspace.id}`} {...clickable}>
<TableCell>
<AvatarData
title={
<Stack direction="row" spacing={0} alignItems="center">
{workspace.name}
{workspace.outdated && (
<WorkspaceOutdatedTooltip
templateName={workspace.template_name}
templateId={workspace.template_id}
onUpdateVersion={() => {
onUpdateWorkspace(workspace)
}}
/>
)}
</Stack>
}
subtitle={workspace.owner_name}
avatar={
<Avatar
src={workspace.template_icon}
variant={workspace.template_icon ? "square" : undefined}
fitImage={Boolean(workspace.template_icon)}
>
{workspace.name}
</Avatar>
}
/>
</TableCell>
<TableCell>{displayTemplateName}</TableCell>
<TableCell>
<LastUsed lastUsedAt={workspace.last_used_at} />
</TableCell>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && <UnhealthyTooltip />}
</Box>
</TableCell>
<TableCell>
<div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} />
</div>
</TableCell>
</TableRow>
)
}
export const UnhealthyTooltip = () => {
const styles = useStyles()
return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={styles.unhealthyIcon}
buttonClassName={styles.unhealthyButton}
>
<HelpTooltipTitle>Workspace is unhealthy</HelpTooltipTitle>
<HelpTooltipText>
Your workspace is running but some agents are unhealthy.
</HelpTooltipText>
</HelpTooltip>
)
}
const useStyles = makeStyles((theme) => ({
arrowRight: {
color: theme.palette.text.secondary,
width: 20,
height: 20,
},
arrowCell: {
display: "flex",
paddingLeft: theme.spacing(2),
},
unhealthyIcon: {
color: colors.yellow[5],
},
unhealthyButton: {
opacity: 1,
"&:hover": {
opacity: 1,
},
},
}))
@@ -1,54 +0,0 @@
import Table from "@mui/material/Table"
import TableBody from "@mui/material/TableBody"
import TableCell from "@mui/material/TableCell"
import TableContainer from "@mui/material/TableContainer"
import TableHead from "@mui/material/TableHead"
import TableRow from "@mui/material/TableRow"
import { Workspace } from "api/typesGenerated"
import { FC } from "react"
import { WorkspacesTableBody } from "./WorkspacesTableBody"
const Language = {
name: "Name",
template: "Template",
lastUsed: "Last Used",
status: "Status",
}
export interface WorkspacesTableProps {
workspaces?: Workspace[]
isUsingFilter: boolean
onUpdateWorkspace: (workspace: Workspace) => void
error?: unknown
}
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
workspaces,
isUsingFilter,
onUpdateWorkspace,
error,
}) => {
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">{Language.name}</TableCell>
<TableCell width="25%">{Language.template}</TableCell>
<TableCell width="20%">{Language.lastUsed}</TableCell>
<TableCell width="15%">{Language.status}</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
<WorkspacesTableBody
workspaces={workspaces}
isUsingFilter={isUsingFilter}
onUpdateWorkspace={onUpdateWorkspace}
error={error}
/>
</TableBody>
</Table>
</TableContainer>
)
}
@@ -1,95 +0,0 @@
import Button from "@mui/material/Button"
import { makeStyles } from "@mui/styles"
import AddOutlined from "@mui/icons-material/AddOutlined"
import { Workspace } from "api/typesGenerated"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { TableEmpty } from "components/TableEmpty/TableEmpty"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { Link as RouterLink } from "react-router-dom"
import { TableLoaderSkeleton } from "../TableLoader/TableLoader"
import { WorkspacesRow } from "./WorkspacesRow"
interface TableBodyProps {
workspaces?: Workspace[]
isUsingFilter: boolean
onUpdateWorkspace: (workspace: Workspace) => void
error?: unknown
}
export const WorkspacesTableBody: FC<
React.PropsWithChildren<TableBodyProps>
> = ({ workspaces, isUsingFilter, onUpdateWorkspace, error }) => {
const { t } = useTranslation("workspacesPage")
const styles = useStyles()
if (error) {
return <TableEmpty message={t("emptyResultsMessage")} />
}
if (!workspaces) {
return <TableLoaderSkeleton columns={5} useAvatarData />
}
if (workspaces.length === 0) {
return (
<ChooseOne>
<Cond condition={isUsingFilter}>
<TableEmpty message={t("emptyResultsMessage")} />
</Cond>
<Cond>
<TableEmpty
className={styles.withImage}
message={t("emptyCreateWorkspaceMessage")}
description={t("emptyCreateWorkspaceDescription")}
cta={
<Button
component={RouterLink}
to="/templates"
startIcon={<AddOutlined />}
variant="contained"
>
{t("createFromTemplateButton")}
</Button>
}
image={
<div className={styles.emptyImage}>
<img src="/featured/workspaces.webp" alt="" />
</div>
}
/>
</Cond>
</ChooseOne>
)
}
return (
<>
{workspaces.map((workspace) => (
<WorkspacesRow
workspace={workspace}
key={workspace.id}
onUpdateWorkspace={onUpdateWorkspace}
/>
))}
</>
)
}
const useStyles = makeStyles((theme) => ({
withImage: {
paddingBottom: 0,
},
emptyImage: {
maxWidth: "50%",
height: theme.spacing(34),
overflow: "hidden",
marginTop: theme.spacing(6),
opacity: 0.85,
"& img": {
maxWidth: "100%",
},
},
}))
@@ -11,12 +11,52 @@ import { useFilter } from "components/Filter/filter"
import { useUserFilterMenu } from "components/Filter/UserFilter"
const WorkspacesPage: FC = () => {
const orgId = useOrganizationId()
// If we use a useSearchParams for each hook, the values will not be in sync.
// So we have to use a single one, centralizing the values, and pass it to
// each hook.
const searchParamsResult = useSearchParams()
const pagination = usePagination({ searchParamsResult })
const filterProps = useWorkspacesFilter({ searchParamsResult, pagination })
const { data, error, queryKey } = useWorkspacesData({
...pagination,
query: filterProps.filter.query,
})
const updateWorkspace = useWorkspaceUpdate(queryKey)
return (
<>
<Helmet>
<title>{pageTitle("Workspaces")}</title>
</Helmet>
<WorkspacesPageView
workspaces={data?.workspaces}
error={error}
count={data?.count}
page={pagination.page}
limit={pagination.limit}
onPageChange={pagination.goToPage}
filterProps={filterProps}
onUpdateWorkspace={(workspace) => {
updateWorkspace.mutate(workspace)
}}
/>
</>
)
}
export default WorkspacesPage
type UseWorkspacesFilterOptions = {
searchParamsResult: ReturnType<typeof useSearchParams>
pagination: ReturnType<typeof usePagination>
}
const useWorkspacesFilter = ({
searchParamsResult,
pagination,
}: UseWorkspacesFilterOptions) => {
const orgId = useOrganizationId()
const filter = useFilter({
initialValue: `owner:me`,
searchParamsResult,
@@ -24,11 +64,6 @@ const WorkspacesPage: FC = () => {
pagination.goToPage(1)
},
})
const { data, error, queryKey } = useWorkspacesData({
...pagination,
query: filter.query,
})
const updateWorkspace = useWorkspaceUpdate(queryKey)
const permissions = usePermissions()
const canFilterByUser = permissions.viewDeploymentValues
const userMenu = useUserFilterMenu({
@@ -49,33 +84,12 @@ const WorkspacesPage: FC = () => {
filter.update({ ...filter.values, status: option?.value }),
})
return (
<>
<Helmet>
<title>{pageTitle("Workspaces")}</title>
</Helmet>
<WorkspacesPageView
workspaces={data?.workspaces}
error={error}
count={data?.count}
page={pagination.page}
limit={pagination.limit}
onPageChange={pagination.goToPage}
filterProps={{
filter,
menus: {
user: canFilterByUser ? userMenu : undefined,
template: templateMenu,
status: statusMenu,
},
}}
onUpdateWorkspace={(workspace) => {
updateWorkspace.mutate(workspace)
}}
/>
</>
)
return {
filter,
menus: {
user: canFilterByUser ? userMenu : undefined,
template: templateMenu,
status: statusMenu,
},
}
}
export default WorkspacesPage
@@ -12,7 +12,7 @@ import {
} from "components/PageHeader/PageHeader"
import { Stack } from "components/Stack/Stack"
import { WorkspaceHelpTooltip } from "components/Tooltips"
import { WorkspacesTable } from "components/WorkspacesTable/WorkspacesTable"
import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable"
import { useLocalStorage } from "hooks"
import difference from "lodash/difference"
import { ImpendingDeletionBanner, Count } from "components/WorkspaceDeletion"
@@ -130,7 +130,6 @@ export const WorkspacesPageView: FC<
workspaces={workspaces}
isUsingFilter={filterProps.filter.used}
onUpdateWorkspace={onUpdateWorkspace}
error={error}
/>
{count !== undefined && (
<PaginationWidgetBase
@@ -0,0 +1,230 @@
import Table from "@mui/material/Table"
import TableBody from "@mui/material/TableBody"
import TableCell from "@mui/material/TableCell"
import TableContainer from "@mui/material/TableContainer"
import TableHead from "@mui/material/TableHead"
import TableRow from "@mui/material/TableRow"
import { Workspace } from "api/typesGenerated"
import { FC, ReactNode } from "react"
import { TableEmpty } from "components/TableEmpty/TableEmpty"
import { useTranslation } from "react-i18next"
import { TableLoaderSkeleton } from "components/TableLoader/TableLoader"
import AddOutlined from "@mui/icons-material/AddOutlined"
import Button from "@mui/material/Button"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { Link as RouterLink, useNavigate } from "react-router-dom"
import { makeStyles } from "@mui/styles"
import {
HelpTooltip,
HelpTooltipText,
HelpTooltipTitle,
} from "components/Tooltips/HelpTooltip"
import InfoIcon from "@mui/icons-material/InfoOutlined"
import { colors } from "theme/colors"
import { useClickableTableRow } from "hooks/useClickableTableRow"
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
import Box from "@mui/material/Box"
import { AvatarData } from "components/AvatarData/AvatarData"
import { Avatar } from "components/Avatar/Avatar"
import { Stack } from "components/Stack/Stack"
import { LastUsed } from "components/LastUsed/LastUsed"
import { WorkspaceOutdatedTooltip } from "components/Tooltips"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { getDisplayWorkspaceTemplateName } from "utils/workspace"
export interface WorkspacesTableProps {
workspaces?: Workspace[]
isUsingFilter: boolean
onUpdateWorkspace: (workspace: Workspace) => void
error?: unknown
}
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
workspaces,
isUsingFilter,
onUpdateWorkspace,
}) => {
const { t } = useTranslation("workspacesPage")
const styles = useStyles()
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">Name</TableCell>
<TableCell width="25%">Template</TableCell>
<TableCell width="20%">Last used</TableCell>
<TableCell width="15%">Status</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
{!workspaces && <TableLoaderSkeleton columns={5} useAvatarData />}
{workspaces && workspaces.length === 0 && (
<ChooseOne>
<Cond condition={isUsingFilter}>
<TableEmpty message={t("emptyResultsMessage")} />
</Cond>
<Cond>
<TableEmpty
className={styles.withImage}
message={t("emptyCreateWorkspaceMessage")}
description={t("emptyCreateWorkspaceDescription")}
cta={
<Button
component={RouterLink}
to="/templates"
startIcon={<AddOutlined />}
variant="contained"
>
{t("createFromTemplateButton")}
</Button>
}
image={
<div className={styles.emptyImage}>
<img src="/featured/workspaces.webp" alt="" />
</div>
}
/>
</Cond>
</ChooseOne>
)}
{workspaces &&
workspaces.map((workspace) => (
<WorkspacesRow workspace={workspace} key={workspace.id}>
<TableCell>
<AvatarData
title={
<Stack direction="row" spacing={0} alignItems="center">
{workspace.name}
{workspace.outdated && (
<WorkspaceOutdatedTooltip
templateName={workspace.template_name}
templateId={workspace.template_id}
onUpdateVersion={() => {
onUpdateWorkspace(workspace)
}}
/>
)}
</Stack>
}
subtitle={workspace.owner_name}
avatar={
<Avatar
src={workspace.template_icon}
variant={workspace.template_icon ? "square" : undefined}
fitImage={Boolean(workspace.template_icon)}
>
{workspace.name}
</Avatar>
}
/>
</TableCell>
<TableCell>
{getDisplayWorkspaceTemplateName(workspace)}
</TableCell>
<TableCell>
<LastUsed lastUsedAt={workspace.last_used_at} />
</TableCell>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && <UnhealthyTooltip />}
</Box>
</TableCell>
<TableCell>
<Box
sx={{
display: "flex",
paddingLeft: (theme) => theme.spacing(2),
}}
>
<KeyboardArrowRight
sx={{
color: (theme) => theme.palette.text.secondary,
width: 20,
height: 20,
}}
/>
</Box>
</TableCell>
</WorkspacesRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
const WorkspacesRow: FC<{
workspace: Workspace
children: ReactNode
}> = ({ workspace, children }) => {
const navigate = useNavigate()
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
const clickable = useClickableTableRow(() => {
navigate(workspacePageLink)
})
return (
<TableRow data-testid={`workspace-${workspace.id}`} {...clickable}>
{children}
</TableRow>
)
}
export const UnhealthyTooltip = () => {
const styles = useUnhealthyTooltipStyles()
return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={styles.unhealthyIcon}
buttonClassName={styles.unhealthyButton}
>
<HelpTooltipTitle>Workspace is unhealthy</HelpTooltipTitle>
<HelpTooltipText>
Your workspace is running but some agents are unhealthy.
</HelpTooltipText>
</HelpTooltip>
)
}
const useUnhealthyTooltipStyles = makeStyles(() => ({
unhealthyIcon: {
color: colors.yellow[5],
},
unhealthyButton: {
opacity: 1,
"&:hover": {
opacity: 1,
},
},
}))
const useStyles = makeStyles((theme) => ({
withImage: {
paddingBottom: 0,
},
emptyImage: {
maxWidth: "50%",
height: theme.spacing(34),
overflow: "hidden",
marginTop: theme.spacing(6),
opacity: 0.85,
"& img": {
maxWidth: "100%",
},
},
}))