mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add agent metadata (#6614)
This commit is contained in:
@@ -1046,6 +1046,18 @@ const getMissingParameters = (
|
||||
return missingParameters
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param agentId
|
||||
* @returns An EventSource that emits agent metadata event objects (ServerSentEvent)
|
||||
*/
|
||||
export const watchAgentMetadata = (agentId: string): EventSource => {
|
||||
return new EventSource(
|
||||
`${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`,
|
||||
{ withCredentials: true },
|
||||
)
|
||||
}
|
||||
|
||||
export const watchBuildLogs = (
|
||||
versionId: string,
|
||||
onMessage: (log: TypesGen.ProvisionerJobLog) => void,
|
||||
|
||||
@@ -1086,6 +1086,29 @@ export interface WorkspaceAgentListeningPortsResponse {
|
||||
readonly ports: WorkspaceAgentListeningPort[]
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface WorkspaceAgentMetadata {
|
||||
readonly result: WorkspaceAgentMetadataResult
|
||||
readonly description: WorkspaceAgentMetadataDescription
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface WorkspaceAgentMetadataDescription {
|
||||
readonly display_name: string
|
||||
readonly key: string
|
||||
readonly script: string
|
||||
readonly interval: number
|
||||
readonly timeout: number
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface WorkspaceAgentMetadataResult {
|
||||
readonly collected_at: string
|
||||
readonly age: number
|
||||
readonly value: string
|
||||
readonly error: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface WorkspaceAgentStartupLog {
|
||||
readonly id: number
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import {
|
||||
WorkspaceAgentMetadataDescription,
|
||||
WorkspaceAgentMetadataResult,
|
||||
} from "api/typesGenerated"
|
||||
import { AgentMetadataView, AgentMetadataViewProps } from "./AgentMetadata"
|
||||
|
||||
export default {
|
||||
title: "components/AgentMetadata",
|
||||
component: AgentMetadataView,
|
||||
}
|
||||
|
||||
const Template: Story<AgentMetadataViewProps> = (args) => (
|
||||
<AgentMetadataView {...args} />
|
||||
)
|
||||
|
||||
const resultDefaults: WorkspaceAgentMetadataResult = {
|
||||
collected_at: "2021-05-05T00:00:00Z",
|
||||
error: "",
|
||||
value: "defvalue",
|
||||
age: 5,
|
||||
}
|
||||
|
||||
const descriptionDefaults: WorkspaceAgentMetadataDescription = {
|
||||
display_name: "DisPlay",
|
||||
key: "defkey",
|
||||
interval: 10,
|
||||
timeout: 10,
|
||||
script: "some command",
|
||||
}
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
metadata: [
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "110%",
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
display_name: "CPU",
|
||||
key: "CPU",
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "50GB",
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
display_name: "Memory",
|
||||
key: "Memory",
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "cant see it",
|
||||
age: 300,
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
interval: 5,
|
||||
display_name: "Stale",
|
||||
key: "stale",
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "oops",
|
||||
error: "fatal error",
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
display_name: "Error",
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "oops",
|
||||
error: "fatal error",
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
display_name: "Error",
|
||||
key: "stale",
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
...resultDefaults,
|
||||
value: "",
|
||||
collected_at: "0001-01-01T00:00:00Z",
|
||||
age: 1000000,
|
||||
},
|
||||
description: {
|
||||
...descriptionDefaults,
|
||||
display_name: "Never loads",
|
||||
key: "nloads",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import CircularProgress from "@material-ui/core/CircularProgress"
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles"
|
||||
import { watchAgentMetadata } from "api/api"
|
||||
import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated"
|
||||
import { CodeExample } from "components/CodeExample/CodeExample"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import {
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "components/Tooltips/HelpTooltip"
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
export const WatchAgentMetadataContext = createContext(watchAgentMetadata)
|
||||
|
||||
const MetadataItemValue: FC<
|
||||
PropsWithChildren<{ item: WorkspaceAgentMetadata }>
|
||||
> = ({ item, children }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={anchorRef}
|
||||
onMouseEnter={() => setIsOpen(true)}
|
||||
role="presentation"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
PaperProps={{
|
||||
onMouseEnter: () => setIsOpen(true),
|
||||
onMouseLeave: () => setIsOpen(false),
|
||||
}}
|
||||
classes={{ paper: styles.metadataPopover }}
|
||||
>
|
||||
<HelpTooltipTitle>{item.description.display_name}</HelpTooltipTitle>
|
||||
{item.result.value.length > 0 && (
|
||||
<>
|
||||
<HelpTooltipText>Last result:</HelpTooltipText>
|
||||
<HelpTooltipText>
|
||||
<CodeExample code={item.result.value} />
|
||||
</HelpTooltipText>
|
||||
</>
|
||||
)}
|
||||
{item.result.error.length > 0 && (
|
||||
<>
|
||||
<HelpTooltipText>Last error:</HelpTooltipText>
|
||||
<HelpTooltipText>
|
||||
<CodeExample code={item.result.error} />
|
||||
</HelpTooltipText>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const labelAnchorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (item.result === undefined) {
|
||||
throw new Error("Metadata item result is undefined")
|
||||
}
|
||||
if (item.description === undefined) {
|
||||
throw new Error("Metadata item description is undefined")
|
||||
}
|
||||
|
||||
const staleThreshold = Math.max(
|
||||
item.description.interval + item.description.timeout * 2,
|
||||
5,
|
||||
)
|
||||
|
||||
const status: "stale" | "valid" | "loading" = (() => {
|
||||
const year = dayjs(item.result.collected_at).year()
|
||||
if (year <= 1970 || isNaN(year)) {
|
||||
return "loading"
|
||||
}
|
||||
if (item.result.age > staleThreshold) {
|
||||
return "stale"
|
||||
}
|
||||
return "valid"
|
||||
})()
|
||||
|
||||
// Stale data is as good as no data. Plus, we want to build confidence in our
|
||||
// users that what's shown is real. If times aren't correctly synced this
|
||||
// could be buggy. But, how common is that anyways?
|
||||
const value =
|
||||
status === "stale" || status === "loading" ? (
|
||||
<CircularProgress size={12} />
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
styles.metadataValue +
|
||||
" " +
|
||||
(item.result.error.length === 0
|
||||
? styles.metadataValueSuccess
|
||||
: styles.metadataValueError)
|
||||
}
|
||||
>
|
||||
{item.result.value}
|
||||
</div>
|
||||
)
|
||||
|
||||
const updatesInSeconds = -(item.description.interval - item.result.age)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.metadata}>
|
||||
<div
|
||||
className={styles.metadataLabel}
|
||||
onMouseEnter={() => setIsOpen(true)}
|
||||
// onMouseLeave={() => setIsOpen(false)}
|
||||
role="presentation"
|
||||
ref={labelAnchorRef}
|
||||
>
|
||||
{item.description.display_name}
|
||||
</div>
|
||||
<MetadataItemValue item={item}>{value}</MetadataItemValue>
|
||||
</div>
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={isOpen}
|
||||
anchorEl={labelAnchorRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
PaperProps={{
|
||||
onMouseEnter: () => setIsOpen(true),
|
||||
onMouseLeave: () => setIsOpen(false),
|
||||
}}
|
||||
classes={{ paper: styles.metadataPopover }}
|
||||
>
|
||||
<HelpTooltipTitle>{item.description.display_name}</HelpTooltipTitle>
|
||||
{status === "stale" ? (
|
||||
<HelpTooltipText>
|
||||
This item is now stale because the agent hasn{"'"}t reported a new
|
||||
value in {dayjs.duration(item.result.age, "s").humanize()}.
|
||||
</HelpTooltipText>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status === "valid" ? (
|
||||
<HelpTooltipText>
|
||||
The agent collected this value{" "}
|
||||
{dayjs.duration(item.result.age, "s").humanize()} ago and will
|
||||
update it in{" "}
|
||||
{dayjs.duration(Math.min(updatesInSeconds, 0), "s").humanize()}.
|
||||
</HelpTooltipText>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status === "loading" ? (
|
||||
<HelpTooltipText>
|
||||
This value is loading for the first time...
|
||||
</HelpTooltipText>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<HelpTooltipText>
|
||||
This value is produced by the following script:
|
||||
</HelpTooltipText>
|
||||
<HelpTooltipText>
|
||||
<CodeExample code={item.description.script}></CodeExample>
|
||||
</HelpTooltipText>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AgentMetadataViewProps {
|
||||
metadata: WorkspaceAgentMetadata[]
|
||||
}
|
||||
|
||||
export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
|
||||
const styles = useStyles()
|
||||
if (metadata.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<Stack
|
||||
alignItems="flex-start"
|
||||
direction="row"
|
||||
spacing={5}
|
||||
className={styles.metadataStack}
|
||||
>
|
||||
<div className={styles.metadataHeader}>
|
||||
{metadata.map((m) => {
|
||||
if (m.description === undefined) {
|
||||
throw new Error("Metadata item description is undefined")
|
||||
}
|
||||
return <MetadataItem key={m.description.key} item={m} />
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export const AgentMetadata: FC<{
|
||||
agent: WorkspaceAgent
|
||||
}> = ({ agent }) => {
|
||||
const [metadata, setMetadata] = useState<
|
||||
WorkspaceAgentMetadata[] | undefined
|
||||
>(undefined)
|
||||
|
||||
const watchAgentMetadata = useContext(WatchAgentMetadataContext)
|
||||
|
||||
useEffect(() => {
|
||||
const source = watchAgentMetadata(agent.id)
|
||||
|
||||
source.onerror = (e) => {
|
||||
console.error("received error in watch stream", e)
|
||||
}
|
||||
source.addEventListener("data", (e) => {
|
||||
const data = JSON.parse(e.data)
|
||||
setMetadata(data)
|
||||
})
|
||||
return () => {
|
||||
source.close()
|
||||
}
|
||||
}, [agent.id, watchAgentMetadata])
|
||||
|
||||
if (metadata === undefined) {
|
||||
return <CircularProgress size={16} />
|
||||
}
|
||||
|
||||
return <AgentMetadataView metadata={metadata} />
|
||||
}
|
||||
|
||||
// These are more or less copied from
|
||||
// site/src/components/Resources/ResourceCard.tsx
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
metadataStack: {
|
||||
border: `2px dashed ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
width: "100%",
|
||||
},
|
||||
metadataHeader: {
|
||||
padding: "8px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
||||
gap: theme.spacing(5),
|
||||
rowGap: theme.spacing(3),
|
||||
},
|
||||
|
||||
metadata: {
|
||||
fontSize: 16,
|
||||
},
|
||||
|
||||
metadataLabel: {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
|
||||
metadataValue: {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
|
||||
metadataValueSuccess: {
|
||||
color: theme.palette.success.light,
|
||||
},
|
||||
metadataValueError: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
|
||||
metadataPopover: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
padding: theme.spacing(2.5),
|
||||
color: theme.palette.text.secondary,
|
||||
pointerEvents: "auto",
|
||||
maxWidth: "480px",
|
||||
|
||||
"& .MuiButton-root": {
|
||||
padding: theme.spacing(1, 2),
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
|
||||
"&:hover": {
|
||||
background: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -35,6 +35,7 @@ import { SSHButton } from "../SSHButton/SSHButton"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
||||
import { AgentLatency } from "./AgentLatency"
|
||||
import { AgentMetadata } from "./AgentMetadata"
|
||||
import { AgentStatus } from "./AgentStatus"
|
||||
import { AgentVersion } from "./AgentVersion"
|
||||
|
||||
@@ -169,178 +170,202 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
className={styles.agentRow}
|
||||
spacing={4}
|
||||
style={{
|
||||
justifyContent: "none",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<div className={styles.agentStatusWrapper}>
|
||||
<AgentStatus agent={agent} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
<div className={styles.agentStatusWrapper}>
|
||||
<AgentStatus agent={agent} />
|
||||
</div>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="flex-start"
|
||||
key={agent.id}
|
||||
spacing={2}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<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}
|
||||
onUpdate={onUpdateAgent}
|
||||
/>
|
||||
</Maybe>
|
||||
|
||||
<AgentLatency agent={agent} />
|
||||
|
||||
<Maybe condition={agent.status === "connecting"}>
|
||||
<Skeleton width={160} variant="text" />
|
||||
<Skeleton width={36} variant="text" />
|
||||
</Maybe>
|
||||
|
||||
<Maybe condition={agent.status === "timeout"}>
|
||||
{t("unableToConnect")}
|
||||
</Maybe>
|
||||
</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.slug}
|
||||
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}
|
||||
sshPrefix={sshPrefix}
|
||||
/>
|
||||
)}
|
||||
{!hideVSCodeDesktopButton && (
|
||||
<VSCodeDesktopButton
|
||||
userName={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
folderPath={agent.expanded_directory}
|
||||
/>
|
||||
)}
|
||||
{applicationsHost !== undefined &&
|
||||
applicationsHost !== "" && (
|
||||
<PortForwardButton
|
||||
host={applicationsHost}
|
||||
workspaceName={workspace.name}
|
||||
agentId={agent.id}
|
||||
agentName={agent.name}
|
||||
username={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showApps && agent.status === "connecting" && (
|
||||
<>
|
||||
<AppLinkSkeleton width={84} />
|
||||
<AppLinkSkeleton width={112} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<AgentMetadata agent={agent} />
|
||||
{hasStartupFeatures && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
className={styles.agentData}
|
||||
spacing={1}
|
||||
className={styles.startupLinks}
|
||||
>
|
||||
<span className={styles.agentOS}>{agent.operating_system}</span>
|
||||
|
||||
<Maybe condition={agent.status === "connected"}>
|
||||
<AgentVersion
|
||||
agent={agent}
|
||||
serverVersion={serverVersion}
|
||||
onUpdate={onUpdateAgent}
|
||||
/>
|
||||
</Maybe>
|
||||
|
||||
<AgentLatency agent={agent} />
|
||||
|
||||
<Maybe condition={agent.status === "connecting"}>
|
||||
<Skeleton width={160} variant="text" />
|
||||
<Skeleton width={36} variant="text" />
|
||||
</Maybe>
|
||||
|
||||
<Maybe condition={agent.status === "timeout"}>
|
||||
{t("unableToConnect")}
|
||||
</Maybe>
|
||||
</Stack>
|
||||
|
||||
{hasStartupFeatures && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
spacing={1}
|
||||
className={styles.startupLinks}
|
||||
<Link
|
||||
className={styles.startupLink}
|
||||
variant="body2"
|
||||
onClick={() => {
|
||||
setShowStartupLogs(!showStartupLogs)
|
||||
}}
|
||||
>
|
||||
{showStartupLogs ? (
|
||||
<VisibilityOffOutlined />
|
||||
) : (
|
||||
<VisibilityOutlined />
|
||||
)}
|
||||
{showStartupLogs ? "Hide" : "Show"} Startup Logs
|
||||
</Link>
|
||||
|
||||
{agent.startup_script && (
|
||||
<Link
|
||||
className={styles.startupLink}
|
||||
variant="body2"
|
||||
ref={startupScriptAnchorRef}
|
||||
onClick={() => {
|
||||
setShowStartupLogs(!showStartupLogs)
|
||||
setStartupScriptOpen(!startupScriptOpen)
|
||||
}}
|
||||
>
|
||||
{showStartupLogs ? (
|
||||
<VisibilityOffOutlined />
|
||||
) : (
|
||||
<VisibilityOutlined />
|
||||
)}
|
||||
{showStartupLogs ? "Hide" : "Show"} Startup Logs
|
||||
<PlayCircleOutlined />
|
||||
View Startup Script
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{agent.startup_script && (
|
||||
<Link
|
||||
className={styles.startupLink}
|
||||
variant="body2"
|
||||
ref={startupScriptAnchorRef}
|
||||
onClick={() => {
|
||||
setStartupScriptOpen(!startupScriptOpen)
|
||||
<Popover
|
||||
classes={{
|
||||
paper: styles.startupScriptPopover,
|
||||
}}
|
||||
open={startupScriptOpen}
|
||||
onClose={() => setStartupScriptOpen(false)}
|
||||
anchorEl={startupScriptAnchorRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<SyntaxHighlighter
|
||||
style={darcula}
|
||||
language="shell"
|
||||
showLineNumbers
|
||||
// Use inline styles does not work correctly
|
||||
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
|
||||
codeTagProps={{ style: {} }}
|
||||
customStyle={{
|
||||
background: theme.palette.background.default,
|
||||
maxWidth: 600,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
View Startup Script
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
classes={{
|
||||
paper: styles.startupScriptPopover,
|
||||
}}
|
||||
open={startupScriptOpen}
|
||||
onClose={() => setStartupScriptOpen(false)}
|
||||
anchorEl={startupScriptAnchorRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<SyntaxHighlighter
|
||||
style={darcula}
|
||||
language="shell"
|
||||
showLineNumbers
|
||||
// Use inline styles does not work correctly
|
||||
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
|
||||
codeTagProps={{ style: {} }}
|
||||
customStyle={{
|
||||
background: theme.palette.background.default,
|
||||
maxWidth: 600,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{agent.startup_script || ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</Popover>
|
||||
</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.slug}
|
||||
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}
|
||||
sshPrefix={sshPrefix}
|
||||
/>
|
||||
)}
|
||||
{!hideVSCodeDesktopButton && (
|
||||
<VSCodeDesktopButton
|
||||
userName={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
folderPath={agent.expanded_directory}
|
||||
/>
|
||||
)}
|
||||
{applicationsHost !== undefined && applicationsHost !== "" && (
|
||||
<PortForwardButton
|
||||
host={applicationsHost}
|
||||
workspaceName={workspace.name}
|
||||
agentId={agent.id}
|
||||
agentName={agent.name}
|
||||
username={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showApps && agent.status === "connecting" && (
|
||||
<>
|
||||
<AppLinkSkeleton width={84} />
|
||||
<AppLinkSkeleton width={112} />
|
||||
</>
|
||||
{agent.startup_script || ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</Popover>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{showStartupLogs && (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata"
|
||||
import { ProvisionerJobLog } from "api/typesGenerated"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
|
||||
import { withReactContext } from "storybook-react-context"
|
||||
import EventSource from "eventsourcemock"
|
||||
|
||||
export default {
|
||||
title: "components/Workspace",
|
||||
component: Workspace,
|
||||
argTypes: {},
|
||||
decorators: [
|
||||
withReactContext({
|
||||
Context: WatchAgentMetadataContext,
|
||||
initialState: (_: string): EventSource => {
|
||||
// Need Bruno's help here.
|
||||
return new EventSource()
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const Template: Story<WorkspaceProps> = (args) => <Workspace {...args} />
|
||||
|
||||
Reference in New Issue
Block a user