mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add links to the resource card for workspace applications (#2067)
* fix: Use proper webpack config for dev mode This was broken when improving the build times. The typechecker unfortunately missed it! * feat: Add links to the resource card for workspace applications Fixes #1907 and #805. I'll make this pretty in another PR! * Improve style
This commit is contained in:
+132
-122
@@ -19,6 +19,7 @@ import { WorkspaceBuildPage } from "./pages/WorkspaceBuildPage/WorkspaceBuildPag
|
||||
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
|
||||
import { WorkspaceSchedulePage } from "./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"
|
||||
|
||||
const WorkspaceAppErrorPage = lazy(() => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"))
|
||||
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
|
||||
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
|
||||
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
|
||||
@@ -26,138 +27,147 @@ const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/Creat
|
||||
export const AppRouter: FC = () => (
|
||||
<Suspense fallback={<></>}>
|
||||
<Routes>
|
||||
<Route path="/">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<RequireAuth>
|
||||
<IndexPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="healthz" element={<HealthzPage />} />
|
||||
<Route
|
||||
path="cli-auth"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CliAuthenticationPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="workspaces">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<RequireAuth>
|
||||
<IndexPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="healthz" element={<HealthzPage />} />
|
||||
<Route
|
||||
path="cli-auth"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CliAuthenticationPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="workspaces">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspacesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="new"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CreateWorkspacePage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path=":workspace">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspacePage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="schedule"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<WorkspaceSchedulePage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="templates">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplatesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=":template"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplatePage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="users">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<UsersPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CreateUserPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<SettingsLayout />}>
|
||||
<Route path="account" element={<AccountPage />} />
|
||||
<Route path="security" element={<SecurityPage />} />
|
||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path=":username">
|
||||
<Route path=":workspace">
|
||||
<Route
|
||||
path="terminal"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<TerminalPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="builds/:buildId"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspaceBuildPage />
|
||||
<WorkspacesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
<Route
|
||||
path="new"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CreateWorkspacePage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path=":workspace">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspacePage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="schedule"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<WorkspaceSchedulePage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="templates">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplatesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=":template"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplatePage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="users">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<UsersPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<CreateUserPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<SettingsLayout />}>
|
||||
<Route path="account" element={<AccountPage />} />
|
||||
<Route path="security" element={<SecurityPage />} />
|
||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="builds/:buildId"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspaceBuildPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/@:username">
|
||||
<Route path=":workspace">
|
||||
<Route
|
||||
path="terminal"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<TerminalPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="apps">
|
||||
<Route
|
||||
path=":app/*"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspaceAppErrorPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
acts like a catch-all for URLs that we don't have explicit
|
||||
routes for. */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { MockWorkspace } from "../../testHelpers/renderHelpers"
|
||||
import { AppLink, AppLinkProps } from "./AppLink"
|
||||
|
||||
export default {
|
||||
title: "components/AppLink",
|
||||
component: AppLink,
|
||||
}
|
||||
|
||||
const Template: Story<AppLinkProps> = (args) => <AppLink {...args} />
|
||||
|
||||
export const WithIcon = Template.bind({})
|
||||
WithIcon.args = {
|
||||
userName: "developer",
|
||||
workspaceName: MockWorkspace.name,
|
||||
appName: "code-server",
|
||||
appIcon: "/code.svg",
|
||||
}
|
||||
|
||||
export const WithoutIcon = Template.bind({})
|
||||
WithoutIcon.args = {
|
||||
userName: "developer",
|
||||
workspaceName: MockWorkspace.name,
|
||||
appName: "code-server",
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import React, { FC } from "react"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
|
||||
export interface AppLinkProps {
|
||||
userName: TypesGen.User["username"]
|
||||
workspaceName: TypesGen.Workspace["name"]
|
||||
appName: TypesGen.WorkspaceApp["name"]
|
||||
appIcon: TypesGen.WorkspaceApp["icon"]
|
||||
}
|
||||
|
||||
export const AppLink: FC<AppLinkProps> = ({ userName, workspaceName, appName, appIcon }) => {
|
||||
const styles = useStyles()
|
||||
const href = `/@${userName}/${workspaceName}/apps/${appName}`
|
||||
|
||||
return (
|
||||
<Link href={href} target="_blank" className={styles.link}>
|
||||
<img
|
||||
className={combineClasses([styles.icon, appIcon === "" ? "empty" : ""])}
|
||||
alt={`${appName} Icon`}
|
||||
src={appIcon || ""}
|
||||
/>
|
||||
{appName}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
link: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
|
||||
icon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginRight: theme.spacing(1.5),
|
||||
|
||||
// If no icon is provided we still want the padding on the left
|
||||
// to occur.
|
||||
"&.empty": {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -8,6 +8,8 @@ import useTheme from "@material-ui/styles/useTheme"
|
||||
import { FC } from "react"
|
||||
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { getDisplayAgentStatus } from "../../util/workspace"
|
||||
import { AppLink } from "../AppLink/AppLink"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
|
||||
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
||||
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
|
||||
@@ -83,14 +85,26 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
|
||||
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{agent.status === "connected" && (
|
||||
<TerminalLink
|
||||
className={styles.accessLink}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
userName={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
<Stack>
|
||||
{agent.status === "connected" && (
|
||||
<TerminalLink
|
||||
className={styles.accessLink}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
userName={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
{agent.status === "connected" &&
|
||||
agent.apps.map((app) => (
|
||||
<AppLink
|
||||
key={app.name}
|
||||
appIcon={app.icon}
|
||||
appName={app.name}
|
||||
userName={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface TerminalLinkProps {
|
||||
*/
|
||||
export const TerminalLink: FC<TerminalLinkProps> = ({ agentName, userName = "me", workspaceName, className }) => {
|
||||
const styles = useStyles()
|
||||
const href = `/${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`
|
||||
const href = `/@${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC, useMemo } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { WorkspaceAppErrorPageView } from "./WorkspaceAppErrorPageView"
|
||||
|
||||
const WorkspaceAppErrorView: FC = () => {
|
||||
const { app } = useParams()
|
||||
const message = useMemo(() => {
|
||||
const tag = document.getElementById("api-response")
|
||||
if (!tag) {
|
||||
throw new Error("dev error: api-response meta tag not found")
|
||||
}
|
||||
return tag.getAttribute("data-message") as string
|
||||
}, [])
|
||||
|
||||
return <WorkspaceAppErrorPageView appName={app as string} message={message} />
|
||||
}
|
||||
|
||||
export default WorkspaceAppErrorView
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { WorkspaceAppErrorPageView, WorkspaceAppErrorPageViewProps } from "./WorkspaceAppErrorPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/WorkspaceAppErrorPageView",
|
||||
component: WorkspaceAppErrorPageView,
|
||||
}
|
||||
|
||||
const Template: Story<WorkspaceAppErrorPageViewProps> = (args) => <WorkspaceAppErrorPageView {...args} />
|
||||
|
||||
export const NotRunning = Template.bind({})
|
||||
NotRunning.args = {
|
||||
appName: "code-server",
|
||||
// This is a real message copied and pasted from the backend!
|
||||
message: "remote dial error: dial 'tcp://localhost:13337': dial tcp 127.0.0.1:13337: connect: connection refused",
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { FC } from "react"
|
||||
|
||||
export interface WorkspaceAppErrorPageViewProps {
|
||||
appName: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export const WorkspaceAppErrorPageView: FC<WorkspaceAppErrorPageViewProps> = (props) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<h1 className={styles.title}>{props.appName} is offline!</h1>
|
||||
<p className={styles.message}>{props.message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
flex: 1,
|
||||
padding: theme.spacing(10),
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
},
|
||||
message: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d)">
|
||||
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:overlay" opacity="0.25">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
Reference in New Issue
Block a user