feat: add experimental button to open vscode locally (#5654)

* feat: add experimental button to open vscode locally

This uses the new Coder extension to open up any workspace
with a single click.

* Update site/src/components/VSCodeDesktopButton/VSCodeDesktopButton.stories.tsx

Co-authored-by: Kira Pilot <kira@coder.com>

Co-authored-by: Kira Pilot <kira@coder.com>
This commit is contained in:
Kyle Carberry
2023-01-10 16:07:40 +00:00
committed by GitHub
co-authored by Kira Pilot
parent a23a471034
commit d2ef727064
8 changed files with 249 additions and 0 deletions
@@ -0,0 +1,12 @@
import { Story } from "@storybook/react"
import { VSCodeIcon } from "./VSCodeIcon"
export default {
title: "icons/VSCodeIcon",
component: VSCodeIcon,
}
const Template: Story = (args) => <VSCodeIcon {...args} />
export const Example = Template.bind({})
Example.args = {}
+134
View File
@@ -0,0 +1,134 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const VSCodeIcon: typeof SvgIcon = (props: SvgIconProps) => (
<SvgIcon {...props} viewBox="0 0 100 100">
<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
fillRule="evenodd"
clipRule="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={{
mixBlendMode: "overlay",
}}
opacity="0.25"
>
<path
fillRule="evenodd"
clipRule="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"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="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"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="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 stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</SvgIcon>
)
@@ -32,6 +32,15 @@ HideSSHButton.args = {
hideSSHButton: true,
}
export const HideVSCodeDesktopButton = Template.bind({})
HideVSCodeDesktopButton.args = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
applicationsHost: "",
showApps: true,
hideVSCodeDesktopButton: true,
}
export const NotShowingApps = Template.bind({})
NotShowingApps.args = {
agent: MockWorkspaceAgent,
@@ -13,6 +13,7 @@ import { Maybe } from "components/Conditionals/Maybe"
import { AgentStatus } from "./AgentStatus"
import { AppLinkSkeleton } from "components/AppLink/AppLinkSkeleton"
import { useTranslation } from "react-i18next"
import { VSCodeDesktopButton } from "components/VSCodeDesktopButton/VSCodeDesktopButton"
export interface AgentRowProps {
agent: WorkspaceAgent
@@ -20,6 +21,7 @@ export interface AgentRowProps {
applicationsHost: string | undefined
showApps: boolean
hideSSHButton?: boolean
hideVSCodeDesktopButton?: boolean
serverVersion: string
}
@@ -29,6 +31,7 @@ export const AgentRow: FC<AgentRowProps> = ({
applicationsHost,
showApps,
hideSSHButton,
hideVSCodeDesktopButton,
serverVersion,
}) => {
const styles = useStyles()
@@ -105,6 +108,13 @@ export const AgentRow: FC<AgentRowProps> = ({
agentName={agent.name}
/>
)}
{!hideVSCodeDesktopButton && (
<VSCodeDesktopButton
userName={workspace.owner_name}
workspaceName={workspace.name}
agentName={agent.name}
/>
)}
{applicationsHost !== undefined && applicationsHost !== "" && (
<PortForwardButton
host={applicationsHost}
@@ -0,0 +1,22 @@
import { Story } from "@storybook/react"
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/renderHelpers"
import {
VSCodeDesktopButton,
VSCodeDesktopButtonProps,
} from "./VSCodeDesktopButton"
export default {
title: "components/VSCodeDesktopButton",
component: VSCodeDesktopButton,
}
const Template: Story<VSCodeDesktopButtonProps> = (args) => (
<VSCodeDesktopButton {...args} />
)
export const Default = Template.bind({})
Default.args = {
userName: MockWorkspace.owner_name,
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
}
@@ -0,0 +1,52 @@
import Button from "@material-ui/core/Button"
import { getApiKey } from "api/api"
import { VSCodeIcon } from "components/Icons/VSCodeIcon"
import { FC, PropsWithChildren, useState } from "react"
export interface VSCodeDesktopButtonProps {
userName: string
workspaceName: string
agentName?: string
}
export const VSCodeDesktopButton: FC<
PropsWithChildren<VSCodeDesktopButtonProps>
> = ({ userName, workspaceName, agentName }) => {
const [loading, setLoading] = useState(false)
return (
<Button
startIcon={<VSCodeIcon />}
size="small"
disabled={loading}
onClick={() => {
setLoading(true)
getApiKey()
.then(({ key }) => {
const query = new URLSearchParams({
owner: userName,
workspace: workspaceName,
url: location.origin,
token: key,
})
if (agentName) {
query.set("agent", agentName)
}
window.open(
`vscode://coder.coder-remote/open?${query.toString()}`,
"_blank",
)
})
.catch((ex) => {
console.error(ex)
})
.finally(() => {
setLoading(false)
})
}}
>
VS Code Desktop
</Button>
)
}
@@ -50,6 +50,7 @@ export interface WorkspaceProps {
builds?: TypesGen.WorkspaceBuild[]
canUpdateWorkspace: boolean
hideSSHButton?: boolean
hideVSCodeDesktopButton?: boolean
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
buildInfo?: TypesGen.BuildInfoResponse
applicationsHost?: string
@@ -75,6 +76,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
canUpdateWorkspace,
workspaceErrors,
hideSSHButton,
hideVSCodeDesktopButton,
buildInfo,
applicationsHost,
template,
@@ -215,6 +217,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
applicationsHost={applicationsHost}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
serverVersion={serverVersion}
/>
)}
@@ -42,6 +42,10 @@ export const WorkspaceReadyPage = ({
workspaceState.children["scheduleBannerMachine"],
)
const xServices = useContext(XServiceContext)
const experimental = useSelector(
xServices.entitlementsXService,
(state) => state.context.entitlements.experimental,
)
const featureVisibility = useSelector(
xServices.entitlementsXService,
selectFeatureVisibility,
@@ -120,6 +124,9 @@ export const WorkspaceReadyPage = ({
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]}
hideVSCodeDesktopButton={
!experimental || featureVisibility[FeatureNames.BrowserOnly]
}
workspaceErrors={{
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,