Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0bbb1c9

Browse files
committed
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
1 parent 9d72bb6 commit 0bbb1c9

File tree

9 files changed

+298
-95
lines changed

9 files changed

+298
-95
lines changed

site/src/AppRouter.tsx

+96-86
Original file line numberDiff line numberDiff line change
@@ -19,145 +19,155 @@ import { WorkspaceBuildPage } from "./pages/WorkspaceBuildPage/WorkspaceBuildPag
1919
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
2020
import { WorkspaceSchedulePage } from "./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"
2121

22+
const WorkspaceAppErrorPage = lazy(() => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"))
2223
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
2324
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
2425
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
2526

2627
export const AppRouter: FC = () => (
2728
<Suspense fallback={<></>}>
2829
<Routes>
29-
<Route path="/">
30+
<Route
31+
index
32+
element={
33+
<RequireAuth>
34+
<IndexPage />
35+
</RequireAuth>
36+
}
37+
/>
38+
39+
<Route path="login" element={<LoginPage />} />
40+
<Route path="healthz" element={<HealthzPage />} />
41+
<Route
42+
path="cli-auth"
43+
element={
44+
<RequireAuth>
45+
<CliAuthenticationPage />
46+
</RequireAuth>
47+
}
48+
/>
49+
50+
<Route path="workspaces">
3051
<Route
3152
index
3253
element={
33-
<RequireAuth>
34-
<IndexPage />
35-
</RequireAuth>
54+
<AuthAndFrame>
55+
<WorkspacesPage />
56+
</AuthAndFrame>
3657
}
3758
/>
3859

39-
<Route path="login" element={<LoginPage />} />
40-
<Route path="healthz" element={<HealthzPage />} />
4160
<Route
42-
path="cli-auth"
61+
path="new"
4362
element={
4463
<RequireAuth>
45-
<CliAuthenticationPage />
64+
<CreateWorkspacePage />
4665
</RequireAuth>
4766
}
4867
/>
4968

50-
<Route path="workspaces">
69+
<Route path=":workspace">
5170
<Route
5271
index
5372
element={
5473
<AuthAndFrame>
55-
<WorkspacesPage />
74+
<WorkspacePage />
5675
</AuthAndFrame>
5776
}
5877
/>
59-
6078
<Route
61-
path="new"
79+
path="schedule"
6280
element={
6381
<RequireAuth>
64-
<CreateWorkspacePage />
82+
<WorkspaceSchedulePage />
6583
</RequireAuth>
6684
}
6785
/>
68-
69-
<Route path=":workspace">
70-
<Route
71-
index
72-
element={
73-
<AuthAndFrame>
74-
<WorkspacePage />
75-
</AuthAndFrame>
76-
}
77-
/>
78-
<Route
79-
path="schedule"
80-
element={
81-
<RequireAuth>
82-
<WorkspaceSchedulePage />
83-
</RequireAuth>
84-
}
85-
/>
86-
</Route>
8786
</Route>
87+
</Route>
8888

89-
<Route path="templates">
90-
<Route
91-
index
92-
element={
93-
<AuthAndFrame>
94-
<TemplatesPage />
95-
</AuthAndFrame>
96-
}
97-
/>
89+
<Route path="templates">
90+
<Route
91+
index
92+
element={
93+
<AuthAndFrame>
94+
<TemplatesPage />
95+
</AuthAndFrame>
96+
}
97+
/>
9898

99-
<Route
100-
path=":template"
101-
element={
102-
<AuthAndFrame>
103-
<TemplatePage />
104-
</AuthAndFrame>
105-
}
106-
/>
107-
</Route>
99+
<Route
100+
path=":template"
101+
element={
102+
<AuthAndFrame>
103+
<TemplatePage />
104+
</AuthAndFrame>
105+
}
106+
/>
107+
</Route>
108108

109-
<Route path="users">
110-
<Route
111-
index
112-
element={
113-
<AuthAndFrame>
114-
<UsersPage />
115-
</AuthAndFrame>
116-
}
117-
/>
109+
<Route path="users">
110+
<Route
111+
index
112+
element={
113+
<AuthAndFrame>
114+
<UsersPage />
115+
</AuthAndFrame>
116+
}
117+
/>
118+
<Route
119+
path="create"
120+
element={
121+
<RequireAuth>
122+
<CreateUserPage />
123+
</RequireAuth>
124+
}
125+
/>
126+
</Route>
127+
128+
<Route path="settings" element={<SettingsLayout />}>
129+
<Route path="account" element={<AccountPage />} />
130+
<Route path="security" element={<SecurityPage />} />
131+
<Route path="ssh-keys" element={<SSHKeysPage />} />
132+
</Route>
133+
134+
<Route
135+
path="builds/:buildId"
136+
element={
137+
<AuthAndFrame>
138+
<WorkspaceBuildPage />
139+
</AuthAndFrame>
140+
}
141+
/>
142+
143+
<Route path="/@:username">
144+
<Route path=":workspace">
118145
<Route
119-
path="create"
146+
path="terminal"
120147
element={
121148
<RequireAuth>
122-
<CreateUserPage />
149+
<TerminalPage />
123150
</RequireAuth>
124151
}
125152
/>
126-
</Route>
127153

128-
<Route path="settings" element={<SettingsLayout />}>
129-
<Route path="account" element={<AccountPage />} />
130-
<Route path="security" element={<SecurityPage />} />
131-
<Route path="ssh-keys" element={<SSHKeysPage />} />
132-
</Route>
133-
134-
<Route path=":username">
135-
<Route path=":workspace">
154+
<Route path="apps">
136155
<Route
137-
path="terminal"
156+
path=":app/*"
138157
element={
139-
<RequireAuth>
140-
<TerminalPage />
141-
</RequireAuth>
158+
<AuthAndFrame>
159+
<WorkspaceAppErrorPage />
160+
</AuthAndFrame>
142161
}
143162
/>
144163
</Route>
145164
</Route>
165+
</Route>
146166

147-
<Route
148-
path="builds/:buildId"
149-
element={
150-
<AuthAndFrame>
151-
<WorkspaceBuildPage />
152-
</AuthAndFrame>
153-
}
154-
/>
155-
156-
{/* Using path="*"" means "match anything", so this route
167+
{/* Using path="*"" means "match anything", so this route
157168
acts like a catch-all for URLs that we don't have explicit
158169
routes for. */}
159-
<Route path="*" element={<NotFoundPage />} />
160-
</Route>
170+
<Route path="*" element={<NotFoundPage />} />
161171
</Routes>
162172
</Suspense>
163173
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Story } from "@storybook/react"
2+
import { MockWorkspace } from "../../testHelpers/renderHelpers"
3+
import { AppLink, AppLinkProps } from "./AppLink"
4+
5+
export default {
6+
title: "components/AppLink",
7+
component: AppLink,
8+
}
9+
10+
const Template: Story<AppLinkProps> = (args) => <AppLink {...args} />
11+
12+
export const WithIcon = Template.bind({})
13+
WithIcon.args = {
14+
userName: "developer",
15+
workspaceName: MockWorkspace.name,
16+
appName: "code-server",
17+
appIcon: "/code.svg",
18+
}
19+
20+
export const WithoutIcon = Template.bind({})
21+
WithoutIcon.args = {
22+
userName: "developer",
23+
workspaceName: MockWorkspace.name,
24+
appName: "code-server",
25+
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Link from "@material-ui/core/Link"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import React, { FC } from "react"
4+
import * as TypesGen from "../../api/typesGenerated"
5+
import { combineClasses } from "../../util/combineClasses"
6+
7+
export interface AppLinkProps {
8+
userName: TypesGen.User["username"]
9+
workspaceName: TypesGen.Workspace["name"]
10+
appName: TypesGen.WorkspaceApp["name"]
11+
appIcon: TypesGen.WorkspaceApp["icon"]
12+
}
13+
14+
export const AppLink: FC<AppLinkProps> = ({ userName, workspaceName, appName, appIcon }) => {
15+
const styles = useStyles()
16+
const href = `/@${userName}/${workspaceName}/apps/${appName}`
17+
18+
return (
19+
<Link href={href} target="_blank" className={styles.link}>
20+
<img
21+
className={combineClasses([styles.icon, appIcon === "" ? "empty" : ""])}
22+
alt={`${appName} Icon`}
23+
src={appIcon || ""}
24+
/>
25+
{appName}
26+
</Link>
27+
)
28+
}
29+
30+
const useStyles = makeStyles((theme) => ({
31+
link: {
32+
color: theme.palette.text.secondary,
33+
display: "flex",
34+
alignItems: "center",
35+
},
36+
37+
icon: {
38+
width: 16,
39+
height: 16,
40+
marginRight: theme.spacing(1.5),
41+
42+
// If no icon is provided we still want the padding on the left
43+
// to occur.
44+
"&.empty": {
45+
opacity: 0,
46+
},
47+
},
48+
}))

site/src/components/Resources/Resources.tsx

+22-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import useTheme from "@material-ui/styles/useTheme"
88
import { FC } from "react"
99
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
1010
import { getDisplayAgentStatus } from "../../util/workspace"
11+
import { AppLink } from "../AppLink/AppLink"
12+
import { Stack } from "../Stack/Stack"
1113
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
1214
import { TerminalLink } from "../TerminalLink/TerminalLink"
1315
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
@@ -83,14 +85,26 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
8385
<span className={styles.operatingSystem}>{agent.operating_system}</span>
8486
</TableCell>
8587
<TableCell>
86-
{agent.status === "connected" && (
87-
<TerminalLink
88-
className={styles.accessLink}
89-
workspaceName={workspace.name}
90-
agentName={agent.name}
91-
userName={workspace.owner_name}
92-
/>
93-
)}
88+
<Stack>
89+
{agent.status === "connected" && (
90+
<TerminalLink
91+
className={styles.accessLink}
92+
workspaceName={workspace.name}
93+
agentName={agent.name}
94+
userName={workspace.owner_name}
95+
/>
96+
)}
97+
{agent.status === "connected" &&
98+
agent.apps.map((app) => (
99+
<AppLink
100+
key={app.name}
101+
appIcon={app.icon}
102+
appName={app.name}
103+
userName={workspace.owner_name}
104+
workspaceName={workspace.name}
105+
/>
106+
))}
107+
</Stack>
94108
</TableCell>
95109
<TableCell>
96110
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>

site/src/components/TerminalLink/TerminalLink.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface TerminalLinkProps {
2626
*/
2727
export const TerminalLink: FC<TerminalLinkProps> = ({ agentName, userName = "me", workspaceName, className }) => {
2828
const styles = useStyles()
29-
const href = `/${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`
29+
const href = `/@${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`
3030

3131
return (
3232
<Link
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { FC, useMemo } from "react"
2+
import { useParams } from "react-router-dom"
3+
import { WorkspaceAppErrorPageView } from "./WorkspaceAppErrorPageView"
4+
5+
const WorkspaceAppErrorView: FC = () => {
6+
const { app } = useParams()
7+
const message = useMemo(() => {
8+
const tag = document.getElementById("api-response")
9+
if (!tag) {
10+
throw new Error("dev error: api-response meta tag not found")
11+
}
12+
return tag.getAttribute("data-message") as string
13+
}, [])
14+
15+
return <WorkspaceAppErrorPageView appName={app as string} message={message} />
16+
}
17+
18+
export default WorkspaceAppErrorView

0 commit comments

Comments
 (0)