refactor: Refactor template resources (#4789)

This commit is contained in:
Bruno Quaresma
2022-10-27 17:27:15 -03:00
committed by GitHub
parent 8282e46813
commit ce2a7d49b1
18 changed files with 645 additions and 507 deletions
+44 -25
View File
@@ -1,5 +1,9 @@
import { Story } from "@storybook/react"
import { MockWorkspace } from "../../testHelpers/renderHelpers"
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
} from "testHelpers/renderHelpers"
import { AppLink, AppLinkProps } from "./AppLink"
export default {
@@ -11,44 +15,59 @@ const Template: Story<AppLinkProps> = (args) => <AppLink {...args} />
export const WithIcon = Template.bind({})
WithIcon.args = {
username: "developer",
workspaceName: MockWorkspace.name,
appName: "code-server",
appIcon: "/icon/code.svg",
appSharingLevel: "owner",
health: "healthy",
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
name: "code-server",
icon: "/icon/code.svg",
sharing_level: "owner",
health: "healthy",
},
agent: MockWorkspaceAgent,
}
export const WithoutIcon = Template.bind({})
WithoutIcon.args = {
username: "developer",
workspaceName: MockWorkspace.name,
appName: "code-server",
appSharingLevel: "owner",
health: "healthy",
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
name: "code-server",
sharing_level: "owner",
health: "healthy",
},
agent: MockWorkspaceAgent,
}
export const HealthDisabled = Template.bind({})
HealthDisabled.args = {
username: "developer",
workspaceName: MockWorkspace.name,
appName: "code-server",
appSharingLevel: "owner",
health: "disabled",
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
name: "code-server",
sharing_level: "owner",
health: "disabled",
},
agent: MockWorkspaceAgent,
}
export const HealthInitializing = Template.bind({})
HealthInitializing.args = {
username: "developer",
workspaceName: MockWorkspace.name,
appName: "code-server",
health: "initializing",
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
name: "code-server",
health: "initializing",
},
agent: MockWorkspaceAgent,
}
export const HealthUnhealthy = Template.bind({})
HealthUnhealthy.args = {
username: "developer",
workspaceName: MockWorkspace.name,
appName: "code-server",
health: "unhealthy",
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
name: "code-server",
health: "unhealthy",
},
agent: MockWorkspaceAgent,
}
+27 -55
View File
@@ -3,14 +3,12 @@ import CircularProgress from "@material-ui/core/CircularProgress"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import Tooltip from "@material-ui/core/Tooltip"
import ComputerIcon from "@material-ui/icons/Computer"
import PublicOutlinedIcon from "@material-ui/icons/PublicOutlined"
import LockOutlinedIcon from "@material-ui/icons/LockOutlined"
import GroupOutlinedIcon from "@material-ui/icons/GroupOutlined"
import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline"
import { FC, PropsWithChildren } from "react"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { generateRandomString } from "../../util/random"
import { BaseIcon } from "./BaseIcon"
import { ShareIcon } from "./ShareIcon"
export const Language = {
appTitle: (appName: string, identifier: string): string =>
@@ -19,76 +17,50 @@ export const Language = {
export interface AppLinkProps {
appsHost?: string
username: TypesGen.User["username"]
workspaceName: TypesGen.Workspace["name"]
agentName: TypesGen.WorkspaceAgent["name"]
appName: TypesGen.WorkspaceApp["name"]
appIcon?: TypesGen.WorkspaceApp["icon"]
appCommand?: TypesGen.WorkspaceApp["command"]
appSubdomain: TypesGen.WorkspaceApp["subdomain"]
appSharingLevel: TypesGen.WorkspaceApp["sharing_level"]
health: TypesGen.WorkspaceApp["health"]
workspace: TypesGen.Workspace
app: TypesGen.WorkspaceApp
agent: TypesGen.WorkspaceAgent
}
export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
export const AppLink: FC<AppLinkProps> = ({
appsHost,
username,
workspaceName,
agentName,
appName,
appIcon,
appCommand,
appSubdomain,
appSharingLevel,
health,
app,
workspace,
agent,
}) => {
const styles = useStyles()
const username = workspace.owner_name
// The backend redirects if the trailing slash isn't included, so we add it
// here to avoid extra roundtrips.
let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent(
appName,
)}/`
if (appCommand) {
href = `/@${username}/${workspaceName}.${agentName}/terminal?command=${encodeURIComponent(
appCommand,
)}`
let href = `/@${username}/${workspace.name}.${
agent.name
}/apps/${encodeURIComponent(app.name)}/`
if (app.command) {
href = `/@${username}/${workspace.name}.${
agent.name
}/terminal?command=${encodeURIComponent(app.command)}`
}
if (appsHost && appSubdomain) {
const subdomain = `${appName}--${agentName}--${workspaceName}--${username}`
if (appsHost && app.subdomain) {
const subdomain = `${app.name}--${agent.name}--${workspace.name}--${username}`
href = `${window.location.protocol}//${appsHost}/`.replace("*", subdomain)
}
let canClick = true
let icon = appIcon ? (
<img alt={`${appName} Icon`} src={appIcon} />
) : (
<ComputerIcon />
)
let shareIcon = <LockOutlinedIcon />
let shareTooltip = "Private, only accessible by you"
if (appSharingLevel === "authenticated") {
shareIcon = <GroupOutlinedIcon />
shareTooltip = "Shared with all authenticated users"
}
if (appSharingLevel === "public") {
shareIcon = <PublicOutlinedIcon />
shareTooltip = "Shared publicly"
}
let icon = <BaseIcon app={app} />
let primaryTooltip = ""
if (health === "initializing") {
if (app.health === "initializing") {
canClick = false
icon = <CircularProgress size={16} />
primaryTooltip = "Initializing..."
}
if (health === "unhealthy") {
if (app.health === "unhealthy") {
canClick = false
icon = <ErrorOutlineIcon className={styles.unhealthyIcon} />
primaryTooltip = "Unhealthy"
}
if (!appsHost && appSubdomain) {
if (!appsHost && app.subdomain) {
canClick = false
icon = <ErrorOutlineIcon className={styles.notConfiguredIcon} />
primaryTooltip =
@@ -99,11 +71,11 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
<Button
size="small"
startIcon={icon}
endIcon={<Tooltip title={shareTooltip}>{shareIcon}</Tooltip>}
endIcon={<ShareIcon app={app} />}
className={styles.button}
disabled={!canClick}
>
<span className={styles.appName}>{appName}</span>
<span className={styles.appName}>{app.name}</span>
</Button>
)
@@ -120,7 +92,7 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
event.preventDefault()
window.open(
href,
Language.appTitle(appName, generateRandomString(12)),
Language.appTitle(app.name, generateRandomString(12)),
"width=900,height=600",
)
}
@@ -0,0 +1,43 @@
import { makeStyles } from "@material-ui/core/styles"
import { Stack } from "components/Stack/Stack"
import { FC } from "react"
import * as TypesGen from "api/typesGenerated"
import { BaseIcon } from "./BaseIcon"
import { ShareIcon } from "./ShareIcon"
export interface AppPreviewProps {
app: TypesGen.WorkspaceApp
}
export const AppPreviewLink: FC<AppPreviewProps> = ({ app }) => {
const styles = useStyles()
return (
<Stack
className={styles.appPreviewLink}
alignItems="center"
direction="row"
spacing={1}
>
<BaseIcon app={app} />
{app.name}
<ShareIcon app={app} />
</Stack>
)
}
const useStyles = makeStyles((theme) => ({
appPreviewLink: {
padding: theme.spacing(0.25, 1.5),
borderRadius: 9999,
border: `1px solid ${theme.palette.divider}`,
color: theme.palette.text.primary,
background: theme.palette.background.paper,
flexShrink: 0,
width: "fit-content",
"& img, & svg": {
width: 14,
},
},
}))
+11
View File
@@ -0,0 +1,11 @@
import { WorkspaceApp } from "api/typesGenerated"
import { FC } from "react"
import ComputerIcon from "@material-ui/icons/Computer"
export const BaseIcon: FC<{ app: WorkspaceApp }> = ({ app }) => {
return app.icon ? (
<img alt={`${app.name} Icon`} src={app.icon} />
) : (
<ComputerIcon />
)
}
+28
View File
@@ -0,0 +1,28 @@
import PublicOutlinedIcon from "@material-ui/icons/PublicOutlined"
import LockOutlinedIcon from "@material-ui/icons/LockOutlined"
import GroupOutlinedIcon from "@material-ui/icons/GroupOutlined"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import Tooltip from "@material-ui/core/Tooltip"
import { useTranslation } from "react-i18next"
export interface ShareIconProps {
app: TypesGen.WorkspaceApp
}
export const ShareIcon: FC<ShareIconProps> = ({ app }) => {
const { t } = useTranslation("agent")
let shareIcon = <LockOutlinedIcon />
let shareTooltip = t("shareTooltip.private")
if (app.sharing_level === "authenticated") {
shareIcon = <GroupOutlinedIcon />
shareTooltip = t("shareTooltip.authenticated")
}
if (app.sharing_level === "public") {
shareIcon = <PublicOutlinedIcon />
shareTooltip = t("shareTooltip.public")
}
return <Tooltip title={shareTooltip}>{shareIcon}</Tooltip>
}
@@ -22,7 +22,6 @@ export const Language = {
export interface BuildsTableProps {
builds?: TypesGen.WorkspaceBuild[]
className?: string
}
const groupBuildsByDate = (builds?: TypesGen.WorkspaceBuild[]) => {
@@ -48,13 +47,12 @@ const groupBuildsByDate = (builds?: TypesGen.WorkspaceBuild[]) => {
export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
builds,
className,
}) => {
const isLoading = !builds
const buildsByDate = groupBuildsByDate(builds)
return (
<TableContainer className={className}>
<TableContainer>
<Table data-testid="builds-table" aria-describedby="builds table">
<TableBody>
{isLoading && <TableLoader />}
@@ -0,0 +1,60 @@
import { Story } from "@storybook/react"
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
} from "testHelpers/entities"
import { AgentRow, AgentRowProps } from "./AgentRow"
export default {
title: "components/AgentRow",
component: AgentRow,
}
const Template: Story<AgentRowProps> = (args) => <AgentRow {...args} />
export const Example = Template.bind({})
Example.args = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
applicationsHost: "",
showApps: true,
}
export const HideSSHButton = Template.bind({})
HideSSHButton.args = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
applicationsHost: "",
showApps: true,
hideSSHButton: true,
}
export const NotShowingApps = Template.bind({})
NotShowingApps.args = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
applicationsHost: "",
showApps: false,
}
export const BunchOfApps = Template.bind({})
BunchOfApps.args = {
...Example.args,
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
workspace: MockWorkspace,
applicationsHost: "",
showApps: true,
}
+148
View File
@@ -0,0 +1,148 @@
import { makeStyles } from "@material-ui/core/styles"
import { Skeleton } from "@material-ui/lab"
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
import { FC } from "react"
import { Workspace, WorkspaceAgent } from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
import { Stack } from "../Stack/Stack"
import { TerminalLink } from "../TerminalLink/TerminalLink"
import { AgentLatency } from "./AgentLatency"
import { AgentVersion } from "./AgentVersion"
import { Maybe } from "components/Conditionals/Maybe"
import { AgentStatus } from "./AgentStatus"
export interface AgentRowProps {
agent: WorkspaceAgent
workspace: Workspace
applicationsHost: string | undefined
showApps: boolean
hideSSHButton?: boolean
serverVersion: string
}
export const AgentRow: FC<AgentRowProps> = ({
agent,
workspace,
applicationsHost,
showApps,
hideSSHButton,
serverVersion,
}) => {
const styles = useStyles()
return (
<Stack
key={agent.id}
direction="row"
alignItems="center"
justifyContent="space-between"
className={styles.agentRow}
spacing={4}
>
<Stack direction="row" alignItems="baseline">
<div className={styles.agentStatusWrapper}>
<AgentStatus agent={agent} />
</div>
<div>
<div className={styles.agentName}>{agent.name}</div>
<Stack
direction="row"
alignItems="baseline"
className={styles.agentData}
spacing={1}
>
<span className={styles.agentOS}>{agent.operating_system}</span>
<Maybe condition={agent.status === "connected"}>
<AgentVersion agent={agent} serverVersion={serverVersion} />
</Maybe>
<AgentLatency agent={agent} />
</Stack>
</div>
</Stack>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
wrap="wrap"
maxWidth="750px"
>
{showApps && agent.status === "connected" && (
<>
{agent.apps.map((app) => (
<AppLink
key={app.name}
appsHost={applicationsHost}
app={app}
agent={agent}
workspace={workspace}
/>
))}
<TerminalLink
workspaceName={workspace.name}
agentName={agent.name}
userName={workspace.owner_name}
/>
{!hideSSHButton && (
<SSHButton
workspaceName={workspace.name}
agentName={agent.name}
/>
)}
{applicationsHost !== undefined && (
<PortForwardButton
host={applicationsHost}
workspaceName={workspace.name}
agentId={agent.id}
agentName={agent.name}
username={workspace.owner_name}
/>
)}
</>
)}
{showApps && agent.status === "connecting" && (
<>
<Skeleton width={80} height={36} variant="rect" />
<Skeleton width={120} height={36} variant="rect" />
</>
)}
</Stack>
</Stack>
)
}
const useStyles = makeStyles((theme) => ({
agentRow: {
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paperLight,
fontSize: 16,
"&:not(:last-child)": {
borderBottom: `1px solid ${theme.palette.divider}`,
},
},
agentStatusWrapper: {
width: theme.spacing(4.5),
display: "flex",
justifyContent: "center",
},
agentName: {
fontWeight: 600,
},
agentOS: {
textTransform: "capitalize",
},
agentData: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
},
}))
@@ -0,0 +1,35 @@
import { Story } from "@storybook/react"
import { MockWorkspaceAgent, MockWorkspaceApp } from "testHelpers/entities"
import { AgentRowPreview, AgentRowPreviewProps } from "./AgentRowPreview"
export default {
title: "components/AgentRowPreview",
component: AgentRowPreview,
}
const Template: Story<AgentRowPreviewProps> = (args) => (
<AgentRowPreview {...args} />
)
export const Example = Template.bind({})
Example.args = {
agent: MockWorkspaceAgent,
}
export const BunchOfApps = Template.bind({})
BunchOfApps.args = {
...Example.args,
agent: {
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
}
@@ -0,0 +1,161 @@
import { makeStyles } from "@material-ui/core/styles"
import { AppPreviewLink } from "components/AppLink/AppPreviewLink"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { combineClasses } from "util/combineClasses"
import { WorkspaceAgent } from "../../api/typesGenerated"
import { Stack } from "../Stack/Stack"
export interface AgentRowPreviewProps {
agent: WorkspaceAgent
}
export const AgentRowPreview: FC<AgentRowPreviewProps> = ({ agent }) => {
const styles = useStyles()
const { t } = useTranslation("agent")
return (
<Stack
key={agent.id}
direction="row"
alignItems="center"
justifyContent="space-between"
className={styles.agentRow}
>
<Stack direction="row" alignItems="baseline">
<div className={styles.agentStatusWrapper}>
<div className={styles.agentStatusPreview}></div>
</div>
<Stack
alignItems="baseline"
direction="row"
spacing={4}
className={styles.agentData}
>
<Stack
direction="row"
alignItems="baseline"
spacing={1}
className={combineClasses([styles.noShrink, styles.agentDataItem])}
>
<span>{t("labels.agent").toString()}:</span>
<span className={styles.agentDataValue}>{agent.name}</span>
</Stack>
<Stack
direction="row"
alignItems="baseline"
spacing={1}
className={combineClasses([styles.noShrink, styles.agentDataItem])}
>
<span>{t("labels.os").toString()}:</span>
<span
className={combineClasses([
styles.agentDataValue,
styles.agentOS,
])}
>
{agent.operating_system}
</span>
</Stack>
<Stack
direction="row"
alignItems="center"
spacing={1}
className={styles.agentDataItem}
>
<span>{t("labels.apps").toString()}:</span>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
wrap="wrap"
>
{agent.apps.map((app) => (
<AppPreviewLink key={app.name} app={app} />
))}
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
)
}
const useStyles = makeStyles((theme) => ({
agentRow: {
padding: theme.spacing(2, 4),
backgroundColor: theme.palette.background.paperLight,
fontSize: 16,
position: "relative",
"&:not(:last-child)": {
paddingBottom: 0,
},
"&:after": {
content: "''",
height: "100%",
width: 2,
backgroundColor: theme.palette.divider,
position: "absolute",
top: 0,
left: 49,
},
},
agentStatusWrapper: {
width: theme.spacing(4.5),
display: "flex",
justifyContent: "center",
flexShrink: 0,
},
agentStatusPreview: {
width: 10,
height: 10,
border: `2px solid ${theme.palette.text.secondary}`,
borderRadius: "100%",
position: "relative",
zIndex: 1,
background: theme.palette.background.paper,
},
agentName: {
fontWeight: 600,
},
agentOS: {
textTransform: "capitalize",
fontSize: 14,
color: theme.palette.text.secondary,
},
agentData: {
fontSize: 14,
color: theme.palette.text.secondary,
[theme.breakpoints.down("sm")]: {
gap: theme.spacing(2),
flexWrap: "wrap",
},
},
agentDataValue: {
color: theme.palette.text.primary,
},
noShrink: {
flexShrink: 0,
},
agentDataItem: {
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing(1),
width: "fit-content",
},
},
}))
@@ -1,10 +1,6 @@
import { Story } from "@storybook/react"
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
MockWorkspaceResource,
} from "testHelpers/entities"
import { MockWorkspace, MockWorkspaceResource } from "testHelpers/entities"
import { AgentRow } from "./AgentRow"
import { ResourceCard, ResourceCardProps } from "./ResourceCard"
export default {
@@ -17,46 +13,16 @@ const Template: Story<ResourceCardProps> = (args) => <ResourceCard {...args} />
export const Example = Template.bind({})
Example.args = {
resource: MockWorkspaceResource,
workspace: MockWorkspace,
applicationsHost: "https://dev.coder.com",
hideSSHButton: false,
showApps: true,
serverVersion: MockWorkspaceAgent.version,
}
export const NotShowingApps = Template.bind({})
NotShowingApps.args = {
...Example.args,
showApps: false,
}
export const HideSSHButton = Template.bind({})
HideSSHButton.args = {
...Example.args,
hideSSHButton: true,
}
export const BunchOfApps = Template.bind({})
BunchOfApps.args = {
...Example.args,
resource: {
...MockWorkspaceResource,
agents: [
{
...MockWorkspaceAgent,
apps: [
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
MockWorkspaceApp,
],
},
],
},
agentRow: (agent) => (
<AgentRow
showApps
key={agent.id}
agent={agent}
workspace={MockWorkspace}
applicationsHost=""
serverVersion=""
/>
),
}
export const BunchOfMetadata = Template.bind({})
@@ -102,4 +68,14 @@ BunchOfMetadata.args = {
},
],
},
agentRow: (agent) => (
<AgentRow
showApps
key={agent.id}
agent={agent}
workspace={MockWorkspace}
applicationsHost=""
serverVersion=""
/>
),
}
+4 -148
View File
@@ -1,16 +1,9 @@
import { makeStyles } from "@material-ui/core/styles"
import { Skeleton } from "@material-ui/lab"
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
import { FC, useState } from "react"
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
import { WorkspaceAgent, WorkspaceResource } from "../../api/typesGenerated"
import { Stack } from "../Stack/Stack"
import { TerminalLink } from "../TerminalLink/TerminalLink"
import { ResourceAvatar } from "./ResourceAvatar"
import { SensitiveValue } from "./SensitiveValue"
import { AgentLatency } from "./AgentLatency"
import { AgentVersion } from "./AgentVersion"
import {
OpenDropdown,
CloseDropdown,
@@ -19,25 +12,13 @@ import IconButton from "@material-ui/core/IconButton"
import Tooltip from "@material-ui/core/Tooltip"
import { Maybe } from "components/Conditionals/Maybe"
import { CopyableValue } from "components/CopyableValue/CopyableValue"
import { AgentStatus } from "./AgentStatus"
export interface ResourceCardProps {
resource: WorkspaceResource
workspace: Workspace
applicationsHost: string | undefined
showApps: boolean
hideSSHButton?: boolean
serverVersion: string
agentRow: (agent: WorkspaceAgent) => JSX.Element
}
export const ResourceCard: FC<ResourceCardProps> = ({
resource,
workspace,
applicationsHost,
showApps,
hideSSHButton,
serverVersion,
}) => {
export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] =
useState(false)
const styles = useStyles()
@@ -113,102 +94,7 @@ export const ResourceCard: FC<ResourceCardProps> = ({
</Stack>
{resource.agents && resource.agents.length > 0 && (
<div>
{resource.agents.map((agent) => {
return (
<Stack
key={agent.id}
direction="row"
alignItems="center"
justifyContent="space-between"
className={styles.agentRow}
>
<Stack direction="row" alignItems="baseline">
<div className={styles.agentStatusWrapper}>
<AgentStatus agent={agent} />
</div>
<div>
<div className={styles.agentName}>{agent.name}</div>
<Stack
direction="row"
alignItems="baseline"
className={styles.agentData}
spacing={1}
>
<span className={styles.agentOS}>
{agent.operating_system}
</span>
<Maybe condition={agent.status === "connected"}>
<AgentVersion
agent={agent}
serverVersion={serverVersion}
/>
</Maybe>
<AgentLatency agent={agent} />
</Stack>
</div>
</Stack>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
wrap="wrap"
maxWidth="750px"
>
{showApps && agent.status === "connected" && (
<>
{agent.apps.map((app) => (
<AppLink
key={app.name}
appsHost={applicationsHost}
appIcon={app.icon}
appName={app.name}
appCommand={app.command}
appSubdomain={app.subdomain}
username={workspace.owner_name}
workspaceName={workspace.name}
agentName={agent.name}
health={app.health}
appSharingLevel={app.sharing_level}
/>
))}
<TerminalLink
workspaceName={workspace.name}
agentName={agent.name}
userName={workspace.owner_name}
/>
{!hideSSHButton && (
<SSHButton
workspaceName={workspace.name}
agentName={agent.name}
/>
)}
{applicationsHost !== undefined && (
<PortForwardButton
host={applicationsHost}
workspaceName={workspace.name}
agentId={agent.id}
agentName={agent.name}
username={workspace.owner_name}
/>
)}
</>
)}
{showApps && agent.status === "connecting" && (
<>
<Skeleton width={80} height={36} variant="rect" />
<Skeleton width={120} height={36} variant="rect" />
</>
)}
</Stack>
</Stack>
)
})}
</div>
<div>{resource.agents.map(agentRow)}</div>
)}
</div>
)
@@ -270,34 +156,4 @@ const useStyles = makeStyles((theme) => ({
overflow: "hidden",
whiteSpace: "nowrap",
},
agentRow: {
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paperLight,
fontSize: 16,
"&:not(:last-child)": {
borderBottom: `1px solid ${theme.palette.divider}`,
},
},
agentStatusWrapper: {
width: theme.spacing(4.5),
display: "flex",
justifyContent: "center",
},
agentName: {
fontWeight: 600,
},
agentOS: {
textTransform: "capitalize",
},
agentData: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
},
}))
+10 -37
View File
@@ -5,13 +5,8 @@ import {
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { FC, useState } from "react"
import {
BuildInfoResponse,
Workspace,
WorkspaceResource,
} from "../../api/typesGenerated"
import { WorkspaceAgent, WorkspaceResource } from "../../api/typesGenerated"
import { Stack } from "../Stack/Stack"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { ResourceCard } from "./ResourceCard"
const countAgents = (resource: WorkspaceResource) => {
@@ -20,24 +15,13 @@ const countAgents = (resource: WorkspaceResource) => {
interface ResourcesProps {
resources: WorkspaceResource[]
getResourcesError?: Error | unknown
workspace: Workspace
canUpdateWorkspace: boolean
buildInfo?: BuildInfoResponse | undefined
hideSSHButton?: boolean
applicationsHost?: string
agentRow: (agent: WorkspaceAgent) => JSX.Element
}
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
resources,
getResourcesError,
workspace,
canUpdateWorkspace,
hideSSHButton,
applicationsHost,
buildInfo,
agentRow,
}) => {
const serverVersion = buildInfo?.version || ""
const styles = useStyles()
const [shouldDisplayHideResources, setShouldDisplayHideResources] =
useState(false)
@@ -49,26 +33,15 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
.sort((a, b) => countAgents(b) - countAgents(a))
const hasHideResources = resources.some((r) => r.hide)
if (getResourcesError) {
return <AlertBanner severity="error" error={getResourcesError} />
}
return (
<Stack direction="column" spacing={0}>
{displayResources.map((resource) => {
return (
<ResourceCard
key={resource.id}
resource={resource}
workspace={workspace}
applicationsHost={applicationsHost}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
serverVersion={serverVersion}
/>
)
})}
{displayResources.map((resource) => (
<ResourceCard
key={resource.id}
resource={resource}
agentRow={agentRow}
/>
))}
{hasHideResources && (
<div className={styles.buttonWrapper}>
<Button
@@ -1,44 +0,0 @@
import { fireEvent, render, screen } from "@testing-library/react"
import { FC } from "react"
import { WrapperComponent } from "../../testHelpers/renderHelpers"
import { Language as AgentTooltipLanguage } from "../Tooltips/AgentHelpTooltip"
import { Language as ResourceTooltipLanguage } from "../Tooltips/ResourcesHelpTooltip"
import {
TemplateResourcesProps,
TemplateResourcesTable,
} from "./TemplateResourcesTable"
const Component: FC<React.PropsWithChildren<TemplateResourcesProps>> = (
props,
) => (
<WrapperComponent>
<TemplateResourcesTable {...props} />
</WrapperComponent>
)
describe("TemplateResourcesTable", () => {
it("displays resources tooltip", () => {
const props: TemplateResourcesProps = {
resources: [],
}
render(<Component {...props} />)
const resourceTooltipButton = screen.getAllByRole("button")[0]
fireEvent.click(resourceTooltipButton)
const resourceTooltipTitle = screen.getByText(
ResourceTooltipLanguage.resourceTooltipTitle,
)
expect(resourceTooltipTitle).toBeDefined()
})
it("displays agent tooltip", () => {
const props: TemplateResourcesProps = {
resources: [],
}
render(<Component {...props} />)
const agentTooltipButton = screen.getAllByRole("button")[1]
fireEvent.click(agentTooltipButton)
const agentTooltipTitle = screen.getByText(
AgentTooltipLanguage.agentTooltipTitle,
)
expect(agentTooltipTitle).toBeDefined()
})
})
@@ -1,23 +1,7 @@
import { makeStyles } 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 TableContainer from "@material-ui/core/TableContainer"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { AvatarData } from "components/AvatarData/AvatarData"
import { ResourceAvatar } from "components/Resources/ResourceAvatar"
import { AgentRowPreview } from "components/Resources/AgentRowPreview"
import { Resources } from "components/Resources/Resources"
import { FC } from "react"
import { WorkspaceResource } from "../../api/typesGenerated"
import { Stack } from "../Stack/Stack"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip"
import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip"
export const Language = {
resourceLabel: "Resource",
agentLabel: "Agent",
}
export interface TemplateResourcesProps {
resources: WorkspaceResource[]
@@ -26,109 +10,10 @@ export interface TemplateResourcesProps {
export const TemplateResourcesTable: FC<
React.PropsWithChildren<TemplateResourcesProps>
> = ({ resources }) => {
const styles = useStyles()
return (
<TableContainer>
<Table>
<TableHead>
<TableHeaderRow>
<TableCell>
<Stack direction="row" spacing={0.5} alignItems="center">
{Language.resourceLabel}
<ResourcesHelpTooltip />
</Stack>
</TableCell>
<TableCell className={styles.agentColumn}>
<Stack direction="row" spacing={0.5} alignItems="center">
{Language.agentLabel}
<AgentHelpTooltip />
</Stack>
</TableCell>
</TableHeaderRow>
</TableHead>
<TableBody>
{resources.map((resource) => {
// We need to initialize the agents to display the resource
const agents = resource.agents ?? [null]
return agents.map((agent, agentIndex) => {
// If there is no agent, just display the resource name
if (!agent) {
return (
<TableRow key={resource.id}>
<TableCell className={styles.resourceNameCell}>
<AvatarData
title={resource.name}
subtitle={resource.type}
highlightTitle
avatar={<ResourceAvatar resource={resource} />}
/>
</TableCell>
<TableCell colSpan={3}></TableCell>
</TableRow>
)
}
return (
<TableRow key={`${resource.id}-${agent.id}`}>
{/* We only want to display the name in the first row because we are using rowSpan */}
{/* The rowspan should be the same than the number of agents */}
{agentIndex === 0 && (
<TableCell
className={styles.resourceNameCell}
rowSpan={agents.length}
>
<AvatarData
title={resource.name}
subtitle={resource.type}
highlightTitle
avatar={<ResourceAvatar resource={resource} />}
/>
</TableCell>
)}
<TableCell className={styles.agentColumn}>
{agent.name}
<span className={styles.operatingSystem}>
{agent.operating_system}
</span>
</TableCell>
</TableRow>
)
})
})}
</TableBody>
</Table>
</TableContainer>
<Resources
resources={resources}
agentRow={(agent) => <AgentRowPreview key={agent.id} agent={agent} />}
/>
)
}
const useStyles = makeStyles((theme) => ({
sectionContents: {
margin: 0,
},
resourceNameCell: {
borderRight: `1px solid ${theme.palette.divider}`,
},
resourceType: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
display: "block",
},
// Adds some left spacing
agentColumn: {
paddingLeft: `${theme.spacing(2)}px !important`,
},
operatingSystem: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
display: "block",
textTransform: "capitalize",
},
}))
+31 -28
View File
@@ -16,7 +16,6 @@ import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceScheduleButton"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
import { AlertBanner } from "../AlertBanner/AlertBanner"
import { useTranslation } from "react-i18next"
@@ -24,6 +23,7 @@ import {
EstimateTransitionTime,
WorkspaceBuildProgress,
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
import { AgentRow } from "components/Resources/AgentRow"
export enum WorkspaceErrors {
GET_RESOURCES_ERROR = "getResourcesError",
@@ -87,6 +87,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
const { t } = useTranslation("workspacePage")
const styles = useStyles()
const navigate = useNavigate()
const serverVersion = buildInfo?.version || ""
const hasTemplateIcon =
workspace.template_icon && workspace.template_icon !== ""
@@ -207,32 +208,38 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
/>
)}
{typeof resources !== "undefined" && resources.length > 0 && (
<Resources
resources={resources}
getResourcesError={
workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]
}
workspace={workspace}
canUpdateWorkspace={canUpdateWorkspace}
buildInfo={buildInfo}
hideSSHButton={hideSSHButton}
applicationsHost={applicationsHost}
{Boolean(workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]) && (
<AlertBanner
severity="error"
error={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
/>
)}
<WorkspaceSection
contentsProps={{ className: styles.timelineContents }}
>
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
<AlertBanner
severity="error"
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
/>
) : (
<BuildsTable builds={builds} className={styles.timelineTable} />
)}
</WorkspaceSection>
{typeof resources !== "undefined" && resources.length > 0 && (
<Resources
resources={resources}
agentRow={(agent) => (
<AgentRow
key={agent.id}
agent={agent}
workspace={workspace}
applicationsHost={applicationsHost}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
serverVersion={serverVersion}
/>
)}
/>
)}
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
<AlertBanner
severity="error"
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
/>
) : (
<BuildsTable builds={builds} />
)}
</Stack>
</Margins>
)
@@ -276,9 +283,5 @@ export const useStyles = makeStyles((theme) => {
timelineContents: {
margin: 0,
},
timelineTable: {
border: 0,
},
}
})
+12
View File
@@ -0,0 +1,12 @@
{
"shareTooltip": {
"private": "Private, only accessible by you",
"authenticated": "Shared with all authenticated users",
"public": "Shared publicly"
},
"labels": {
"agent": "Agent",
"os": "OS",
"apps": "Apps"
}
}
+2
View File
@@ -4,6 +4,7 @@ import createWorkspacePage from "./createWorkspacePage.json"
import templatePage from "./templatePage.json"
import templatesPage from "./templatesPage.json"
import workspacePage from "./workspacePage.json"
import agent from "./agent.json"
export const en = {
common,
@@ -12,4 +13,5 @@ export const en = {
templatePage,
templatesPage,
createWorkspacePage,
agent,
}