Skip to content

Commit

Permalink
feat: Add links to the resource card for workspace applications (#2067)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kylecarbs committed Jun 6, 2022
1 parent 722dbab commit ab8235f
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 95 deletions.
182 changes: 96 additions & 86 deletions site/src/AppRouter.tsx
Expand Up @@ -19,145 +19,155 @@ 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"))

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>
<AuthAndFrame>
<WorkspacesPage />
</AuthAndFrame>
}
/>

<Route path="login" element={<LoginPage />} />
<Route path="healthz" element={<HealthzPage />} />
<Route
path="cli-auth"
path="new"
element={
<RequireAuth>
<CliAuthenticationPage />
<CreateWorkspacePage />
</RequireAuth>
}
/>

<Route path="workspaces">
<Route path=":workspace">
<Route
index
element={
<AuthAndFrame>
<WorkspacesPage />
<WorkspacePage />
</AuthAndFrame>
}
/>

<Route
path="new"
path="schedule"
element={
<RequireAuth>
<CreateWorkspacePage />
<WorkspaceSchedulePage />
</RequireAuth>
}
/>

<Route path=":workspace">
<Route
index
element={
<AuthAndFrame>
<WorkspacePage />
</AuthAndFrame>
}
/>
<Route
path="schedule"
element={
<RequireAuth>
<WorkspaceSchedulePage />
</RequireAuth>
}
/>
</Route>
</Route>
</Route>

<Route path="templates">
<Route
index
element={
<AuthAndFrame>
<TemplatesPage />
</AuthAndFrame>
}
/>
<Route path="templates">
<Route
index
element={
<AuthAndFrame>
<TemplatesPage />
</AuthAndFrame>
}
/>

<Route
path=":template"
element={
<AuthAndFrame>
<TemplatePage />
</AuthAndFrame>
}
/>
</Route>
<Route
path=":template"
element={
<AuthAndFrame>
<TemplatePage />
</AuthAndFrame>
}
/>
</Route>

<Route path="users">
<Route
index
element={
<AuthAndFrame>
<UsersPage />
</AuthAndFrame>
}
/>
<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="create"
path="terminal"
element={
<RequireAuth>
<CreateUserPage />
<TerminalPage />
</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="apps">
<Route
path="terminal"
path=":app/*"
element={
<RequireAuth>
<TerminalPage />
</RequireAuth>
<AuthAndFrame>
<WorkspaceAppErrorPage />
</AuthAndFrame>
}
/>
</Route>
</Route>
</Route>

<Route
path="builds/:buildId"
element={
<AuthAndFrame>
<WorkspaceBuildPage />
</AuthAndFrame>
}
/>

{/* Using path="*"" means "match anything", so this 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>
)
25 changes: 25 additions & 0 deletions site/src/components/AppLink/AppLink.stories.tsx
@@ -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",
}
48 changes: 48 additions & 0 deletions site/src/components/AppLink/AppLink.tsx
@@ -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,
},
},
}))
30 changes: 22 additions & 8 deletions site/src/components/Resources/Resources.tsx
Expand Up @@ -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"
Expand Down Expand Up @@ -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 }}>
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/TerminalLink/TerminalLink.tsx
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage.tsx
@@ -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 comments on commit ab8235f

Please sign in to comment.