Archived
1
0

Simplify frontend

Just a login form and a list of applications. No modals or anything like
that.
This commit is contained in:
Asher
2020-02-13 16:38:05 -06:00
parent bf1be16d11
commit b8fa7da972
49 changed files with 984 additions and 1846 deletions

View File

@ -1,82 +0,0 @@
import { getBasepath } from "hookrouter"
import {
Application,
ApplicationsResponse,
CreateSessionResponse,
FilesResponse,
LoginResponse,
RecentResponse,
} from "../common/api"
import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
export interface AuthBody {
password: string
}
/**
* Set authenticated status.
*/
export function setAuthed(authed: boolean): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed(authed)
}
/**
* Try making a request. Throw an error if the request is anything except OK.
* Also set authed to false if the request returns unauthorized.
*/
const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Response> => {
const response = await fetch(getBasepath() + "/api" + endpoint + "/", options)
if (response.status === HttpCode.Unauthorized) {
setAuthed(false)
}
if (response.status !== HttpCode.Ok) {
const text = await response.text()
throw new HttpError(text || response.statusText || "unknown error", response.status)
}
return response
}
/**
* Try authenticating.
*/
export const authenticate = async (body?: AuthBody): Promise<LoginResponse> => {
const response = await tryRequest(ApiEndpoint.login, {
method: "POST",
body: JSON.stringify({ ...body, basePath: getBasepath() }),
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
},
})
return response.json()
}
export const getFiles = async (): Promise<FilesResponse> => {
const response = await tryRequest(ApiEndpoint.files)
return response.json()
}
export const getRecent = async (): Promise<RecentResponse> => {
const response = await tryRequest(ApiEndpoint.recent)
return response.json()
}
export const getApplications = async (): Promise<ApplicationsResponse> => {
const response = await tryRequest(ApiEndpoint.applications)
return response.json()
}
export const getSession = async (app: Application): Promise<CreateSessionResponse> => {
const response = await tryRequest(ApiEndpoint.session, {
method: "POST",
body: JSON.stringify(app),
})
return response.json()
}
export const killSession = async (app: Application): Promise<Response> => {
return tryRequest(ApiEndpoint.session, {
method: "DELETE",
body: JSON.stringify(app),
})
}

View File

@ -1,72 +0,0 @@
import { field, logger } from "@coder/logger"
import { getBasepath, navigate, setBasepath } from "hookrouter"
import * as React from "react"
import { Application, isExecutableApplication } from "../common/api"
import { HttpError } from "../common/http"
import { normalize, Options } from "../common/util"
import { Logo } from "./components/logo"
import { Modal } from "./components/modal"
export interface AppProps {
options: Options
}
interface RedirectedApplication extends Application {
redirected?: boolean
}
let resolved = false
const App: React.FunctionComponent<AppProps> = (props) => {
const [authed, setAuthed] = React.useState<boolean>(props.options.authed)
const [app, setApp] = React.useState<RedirectedApplication | undefined>(props.options.app)
const [error, setError] = React.useState<HttpError | Error | string>()
if (!resolved && typeof document !== "undefined") {
// Get the base path. We need the full URL for connecting the web socket.
// Use the path name plus the provided base path. For example:
// foo.com/base + ./ => foo.com/base
// foo.com/base/ + ./ => foo.com/base
// foo.com/base/bar + ./ => foo.com/base
// foo.com/base/bar/ + ./../ => foo.com/base
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = props.options.basePath
const url = new URL(window.location.origin + "/" + parts.join("/"))
setBasepath(normalize(url.pathname))
logger.debug("resolved base path", field("base", getBasepath()))
resolved = true
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed = (a: boolean): void => {
if (authed !== a) {
setAuthed(a)
}
}
}
React.useEffect(() => {
if (app && !isExecutableApplication(app) && !app.redirected) {
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
setApp({ ...app, redirected: true })
}
}, [app])
return (
<>
{!app || !app.loaded ? (
<div className="coder-splash">
<Logo />
</div>
) : (
undefined
)}
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
{authed && app && app.embedPath && app.redirected ? (
<iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe>
) : (
undefined
)}
</>
)
}
export default App

View File

@ -1,31 +0,0 @@
import * as React from "react"
export interface DelayProps {
readonly show: boolean
readonly delay: number
}
/**
* Animate a component before unmounting (by delaying unmounting) or after
* mounting.
*/
export const Animate: React.FunctionComponent<DelayProps> = (props) => {
const [timer, setTimer] = React.useState<NodeJS.Timeout>()
const [mount, setMount] = React.useState<boolean>(false)
const [visible, setVisible] = React.useState<boolean>(false)
React.useEffect(() => {
if (timer) {
clearTimeout(timer)
}
if (!props.show) {
setVisible(false)
setTimer(setTimeout(() => setMount(false), props.delay))
} else {
setTimer(setTimeout(() => setVisible(true), props.delay))
setMount(true)
}
}, [props])
return mount ? <div className={`animate -${visible ? "show" : "hide"}`}>{props.children}</div> : null
}

View File

@ -1,28 +0,0 @@
.field-error {
color: red;
}
.request-error {
align-items: center;
color: rgba(0, 0, 0, 0.37);
display: flex;
flex: 1;
flex-direction: column;
font-weight: 700;
justify-content: center;
padding: 20px;
text-transform: uppercase;
}
.request-error > .close {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.request-error + .request-error {
border-top: 1px solid #b6b6b6;
}

View File

@ -1,49 +0,0 @@
import * as React from "react"
import { HttpError } from "../../common/http"
export interface ErrorProps {
error: HttpError | Error | string
onClose?: () => void
onCloseText?: string
}
/**
* An error to be displayed in a section where a request has failed.
*/
export const RequestError: React.FunctionComponent<ErrorProps> = (props) => {
return (
<div className="request-error">
<div className="error">{typeof props.error === "string" ? props.error : props.error.message}</div>
{props.onClose ? (
<button className="close" onClick={props.onClose}>
{props.onCloseText || "Close"}
</button>
) : (
undefined
)}
</div>
)
}
/**
* Return a more human/natural/useful message for some error codes resulting
* from a form submission.
*/
const humanizeFormError = (error: HttpError | Error | string): string => {
if (typeof error === "string") {
return error
}
switch ((error as HttpError).code) {
case 401:
return "Wrong password"
default:
return error.message
}
}
/**
* An error to be displayed underneath a field.
*/
export const FieldError: React.FunctionComponent<ErrorProps> = (props) => {
return <div className="field-error">{humanizeFormError(props.error)}</div>
}

View File

@ -1,111 +0,0 @@
.app-list {
list-style-type: none;
padding: 0;
margin: 0 -10px; /* To counter app padding. */
flex: 1;
}
.app-loader {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}
.app-loader > .loader {
color: #b6b6b6;
}
.app-row {
color: #b6b6b6;
display: flex;
font-size: 1em;
line-height: 1em;
width: 100%;
}
.app-row > .launch,
.app-row > .kill {
background-color: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: 1em;
line-height: 1em;
margin: 1px 0;
padding: 3px 10px;
}
.app-row > .launch {
border-radius: 50px;
display: flex;
flex: 1;
}
.app-row > .launch:hover,
.app-row > .kill:hover {
color: #000;
}
.app-row .icon {
height: 1em;
margin-right: 5px;
width: 1em;
}
.app-row .icon.-missing {
background-color: #eee;
color: #b6b6b6;
text-align: center;
}
.app-row .icon.-missing::after {
content: "?";
font-size: 0.7em;
vertical-align: middle;
}
.app-row.-selected {
background-color: #bcc6fa;
}
.app-loader > .opening {
margin-bottom: 10px;
}
.app-loader > .app-row {
color: #000;
justify-content: center;
}
.app-loader > .cancel {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.app-list + .app-list {
border-top: 1px solid #b6b6b6;
margin-top: 1em;
padding-top: 1em;
}
.app-list > .header {
color: #b6b6b6;
font-size: 1em;
margin-bottom: 1em;
margin-top: 0;
}
.app-list > .loader {
color: #b6b6b6;
}
.app-list > .app-row {
cursor: pointer;
}

View File

@ -1,201 +0,0 @@
import * as React from "react"
import { Application, isExecutableApplication, isRunningApplication } from "../../common/api"
import { HttpError } from "../../common/http"
import { getSession, killSession } from "../api"
import { RequestError } from "../components/error"
/**
* An application's details (name and icon).
*/
export const AppDetails: React.FunctionComponent<Application> = (props) => {
return (
<>
{props.icon ? (
<img className="icon" src={`data:image/png;base64,${props.icon}`}></img>
) : (
<div className="icon -missing"></div>
)}
<div className="name">{props.name}</div>
</>
)
}
export interface AppRowProps {
readonly app: Application
onKilled(app: Application): void
open(app: Application): void
}
/**
* A single application row. Can be killed if it's a running application.
*/
export const AppRow: React.FunctionComponent<AppRowProps> = (props) => {
const [killing, setKilling] = React.useState<boolean>(false)
const [error, setError] = React.useState<HttpError>()
function kill(): void {
if (isRunningApplication(props.app)) {
setKilling(true)
killSession(props.app)
.then(() => {
setKilling(false)
props.onKilled(props.app)
})
.catch((error) => {
setError(error)
setKilling(false)
})
}
}
return (
<div className="app-row">
<button className="launch" onClick={(): void => props.open(props.app)}>
<AppDetails {...props.app} />
</button>
{isRunningApplication(props.app) && !killing ? (
<button className="kill" onClick={(): void => kill()}>
{error ? error.message : killing ? "..." : "kill"}
</button>
) : (
undefined
)}
</div>
)
}
export interface AppListProps {
readonly header: string
readonly apps?: ReadonlyArray<Application>
open(app: Application): void
onKilled(app: Application): void
}
/**
* A list of applications. If undefined, show loading text. If empty, show a
* message saying no items are found. Applications can be clicked and killed
* (when applicable).
*/
export const AppList: React.FunctionComponent<AppListProps> = (props) => {
return (
<div className="app-list">
<h2 className="header">{props.header}</h2>
{props.apps && props.apps.length > 0 ? (
props.apps.map((app, i) => <AppRow key={i} app={app} {...props} />)
) : props.apps ? (
<RequestError error={`No ${props.header.toLowerCase()} found`} />
) : (
<div className="loader">loading...</div>
)}
</div>
)
}
export interface ApplicationSection {
readonly apps?: ReadonlyArray<Application>
readonly header: string
}
export interface AppLoaderProps {
readonly app?: Application
setApp(app?: Application): void
getApps(): Promise<ReadonlyArray<ApplicationSection>>
}
/**
* Application sections/groups. Handles loading of the application
* sections, errors, opening applications, and killing applications.
*/
export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
const [apps, setApps] = React.useState<ReadonlyArray<ApplicationSection>>()
const [error, setError] = React.useState<HttpError | Error>()
const refresh = (): void => {
props
.getApps()
.then(setApps)
.catch((e) => setError(e.message))
}
// Every time the component loads go ahead and refresh the list.
React.useEffect(() => {
refresh()
}, [props])
/**
* Open an application if not already open. For executable applications create
* a session first.
*/
function open(app: Application): void {
if (props.app && props.app.name === app.name) {
return setError(new Error(`${app.name} is already open`))
}
props.setApp(app)
if (!isRunningApplication(app) && isExecutableApplication(app)) {
getSession(app)
.then((session) => {
props.setApp({ ...app, ...session })
})
.catch((error) => {
props.setApp(undefined)
setError(error)
})
}
}
// In the case of an error fetching the apps, have the ability to try again.
// In the case of failing to load an app, have the ability to go back to the
// list (where the user can try again if they wish).
if (error) {
return (
<RequestError
error={error}
onCloseText={props.app ? "Go Back" : "Try Again"}
onClose={(): void => {
setError(undefined)
if (!props.app) {
refresh()
}
}}
/>
)
}
// If an app is currently loading, provide the option to cancel.
if (props.app && !props.app.loaded) {
return (
<div className="app-loader">
<div className="opening">Opening</div>
<div className="app-row">
<AppDetails {...props.app} />
</div>
<button
className="cancel"
onClick={(): void => {
props.setApp(undefined)
}}
>
Cancel
</button>
</div>
)
}
// Apps are currently loading.
if (!apps) {
return (
<div className="app-loader">
<div className="loader">loading...</div>
</div>
)
}
// Apps have loaded.
return (
<>
{apps.map((section, i) => (
<AppList key={i} open={open} onKilled={refresh} {...section} />
))}
</>
)
}

View File

@ -1,61 +0,0 @@
import * as React from "react"
export const Logo = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
className="logo"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 172 117"
xmlSpace="preserve"
fill="currentcolor"
{...props}
>
<g>
<g>
<path
className="st0"
d="M164.8,50.9c-3.3,0-5.5-1.9-5.5-5.8V22.7c0-14.3-6-22.2-21.5-22.2h-7.2v15.1h2.2c6.1,0,9,3.3,9,9.2v19.8
c0,8.6,2.6,12.1,8.3,13.9c-5.7,1.7-8.3,5.3-8.3,13.9c0,4.9,0,9.8,0,14.7c0,4.1,0,8.1-1.1,12.2c-1.1,3.8-2.9,7.4-5.4,10.5
c-1.4,1.8-3,3.3-4.8,4.7v2h7.2c15.5,0,21.5-7.9,21.5-22.2V71.9c0-4,2.1-5.8,5.5-5.8h4.1V51h-4V50.9L164.8,50.9z"
/>
<path
className="st0"
d="M115.8,23.3H93.6c-0.5,0-0.9-0.4-0.9-0.9v-1.7c0-0.5,0.4-0.9,0.9-0.9h22.3c0.5,0,0.9,0.4,0.9,0.9v1.7
C116.8,22.9,116.3,23.3,115.8,23.3z"
/>
<path
className="st0"
d="M119.6,44.9h-16.2c-0.5,0-0.9-0.4-0.9-0.9v-1.7c0-0.5,0.4-0.9,0.9-0.9h16.2c0.5,0,0.9,0.4,0.9,0.9V44
C120.5,44.4,120.1,44.9,119.6,44.9z"
/>
<path
className="st0"
d="M126,34.1H93.6c-0.5,0-0.9-0.4-0.9-0.9v-1.7c0-0.5,0.4-0.9,0.9-0.9h32.3c0.5,0,0.9,0.4,0.9,0.9v1.7
C126.8,33.6,126.5,34.1,126,34.1z"
/>
<g>
<path
className="st0"
d="M67.9,28.2c2.2,0,4.4,0.2,6.5,0.7v-4.1c0-5.8,3-9.2,9-9.2h2.2V0.5h-7.2c-15.5,0-21.5,7.9-21.5,22.2v7.4
C60.4,28.9,64.1,28.2,67.9,28.2z"
/>
</g>
<path
className="st0"
d="M132.8,82.6c-1.6-12.7-11.4-23.3-24-25.7c-3.5-0.7-7-0.8-10.4-0.2c-0.1,0-0.1-0.1-0.2-0.1
c-5.5-11.5-17.3-19.1-30.1-19.1S43.6,44.9,38,56.4c-0.1,0-0.1,0.1-0.2,0.1c-3.6-0.4-7.2-0.2-10.8,0.7c-12.4,3-21.8,13.4-23.5,26
c-0.2,1.3-0.3,2.6-0.3,3.8c0,3.8,2.6,7.3,6.4,7.8c4.7,0.7,8.8-2.9,8.7-7.5c0-0.7,0-1.5,0.1-2.2c0.8-6.4,5.7-11.8,12.1-13.3
c2-0.5,4-0.6,5.9-0.3c6.1,0.8,12.1-2.3,14.7-7.7c1.9-4,4.9-7.5,8.9-9.4c4.4-2.1,9.4-2.4,14-0.8c4.8,1.7,8.4,5.3,10.6,9.8
c2.3,4.4,3.4,7.5,8.3,8.1c2,0.3,7.6,0.2,9.7,0.1c4.1,0,8.2,1.4,11.1,4.3c1.9,2,3.3,4.5,3.9,7.3c0.9,4.5-0.2,9-2.9,12.4
c-1.9,2.4-4.5,4.2-7.4,5c-1.4,0.4-2.8,0.5-4.2,0.5c-0.8,0-1.9,0-3.2,0c-4,0-12.5,0-18.9,0c-4.4,0-7.9-3.5-7.9-7.9V78.4V63.9
c0-1.2-1-2.2-2.2-2.2h-3.1c-6.1,0.1-11,6.9-11,14.1s0,26.3,0,26.3c0,7.8,6.3,14.1,14.1,14.1c0,0,34.7-0.1,35.2-0.1
c8-0.8,15.4-4.9,20.4-11.2C131.5,98.8,133.8,90.8,132.8,82.6z"
/>
</g>
</g>
</svg>
)

View File

@ -1,152 +0,0 @@
.modal-bar {
box-sizing: border-box;
display: flex;
justify-content: center;
left: 0;
padding: 20px;
position: fixed;
pointer-events: none;
top: 0;
width: 100%;
z-index: 30;
}
.animate > .modal-bar {
transform: translateY(-100%);
transition: transform 200ms;
}
.animate.-show > .modal-bar {
transform: translateY(0);
}
.modal-bar > .bar {
background-color: #fcfcfc;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
color: #101010;
display: flex;
font-size: 0.8em;
max-width: 400px;
padding: 20px;
pointer-events: initial;
position: relative;
}
.modal-bar > .bar > .content {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
padding-right: 20px;
}
.modal-bar > .bar > .open {
display: flex;
flex-direction: column;
justify-content: center;
}
.modal-bar > .bar > .close {
background-color: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
position: absolute;
right: 1px;
top: 1px;
}
.modal-bar > .bar > .open > .button {
background-color: transparent;
border-radius: 5px;
border: 1px solid #101010;
color: #101010;
cursor: pointer;
padding: 1em;
}
.modal-bar > .bar > .open > .button:hover {
background-color: #bcc6fa;
}
.modal-container {
align-items: center;
background: rgba(0, 0, 0, 0.1);
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
left: 0;
padding: 20px;
position: fixed;
top: 0;
width: 100%;
z-index: 9999999;
}
.modal-container > .modal {
background: #fcfcfc;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row;
height: 100%;
max-height: 400px;
max-width: 664px;
padding: 20px 0;
position: relative;
width: 100%;
}
.sidebar-nav {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 145px;
}
.sidebar-nav > .links {
display: flex;
flex-direction: column;
}
.sidebar-nav > .links > .link {
background-color: transparent;
border: none;
cursor: pointer;
color: rgba(0, 0, 0, 0.37);
font-size: 1.4em;
height: 31px;
margin-bottom: 20px;
padding: 0 35px;
text-decoration: none;
transition: 150ms color ease, 150ms height ease, 150ms margin-bottom ease;
}
.sidebar-nav > .footer > .close {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
width: 100%;
}
.sidebar-nav > .links > .link[aria-current="page"],
.sidebar-nav > .links > .link:hover,
.sidebar-nav > .footer > .close:hover {
color: #000;
}
.modal-container > .modal > .content {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
padding: 0 20px;
}
.modal-container > .modal > .sidebar-nav {
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
}

View File

@ -1,190 +0,0 @@
import { logger } from "@coder/logger"
import * as React from "react"
import { Application, isExecutableApplication } from "../../common/api"
import { HttpError } from "../../common/http"
import { RequestError } from "../components/error"
import { Browse } from "../pages/browse"
import { Home } from "../pages/home"
import { Login } from "../pages/login"
import { Missing } from "../pages/missing"
import { Open } from "../pages/open"
import { Recent } from "../pages/recent"
import { Animate } from "./animate"
export interface ModalProps {
app?: Application
authed: boolean
error?: HttpError | Error | string
setApp(app?: Application): void
setError(error?: HttpError | Error | string): void
}
enum Section {
Browse,
Home,
Open,
Recent,
}
export const Modal: React.FunctionComponent<ModalProps> = (props) => {
const [showModal, setShowModal] = React.useState<boolean>(false)
const [showBar, setShowBar] = React.useState<boolean>(false) // TEMP: Will be true.
const [section, setSection] = React.useState<Section>(Section.Home)
const setApp = (app: Application): void => {
setShowModal(false)
props.setApp(app)
}
React.useEffect(() => {
// Show the bar when hovering around the top area for a while.
let timeout: NodeJS.Timeout | undefined
const hover = (clientY: number): void => {
if (clientY > 30 && timeout) {
clearTimeout(timeout)
timeout = undefined
} else if (clientY <= 30 && !timeout) {
// TEMP: No bar for now.
// timeout = setTimeout(() => setShowBar(true), 1000)
}
}
const iframe =
props.app && !isExecutableApplication(props.app) && (document.getElementById("iframe") as HTMLIFrameElement)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const postIframeMessage = (message: any): void => {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(message, window.location.origin)
} else {
logger.warn("Tried to post message to missing iframe")
}
}
const onHover = (event: MouseEvent | MessageEvent): void => {
hover((event as MessageEvent).data ? (event as MessageEvent).data.clientY : (event as MouseEvent).clientY)
}
const onIframeLoaded = (): void => {
if (props.app) {
setApp({ ...props.app, loaded: true })
}
}
// No need to track the mouse if we don't have a hidden bar.
const hasHiddenBar = !props.error && !showModal && props.app && !showBar
if (props.app && !isExecutableApplication(props.app)) {
// Once the iframe reports it has loaded, tell it to bind mousemove and
// start listening for that instead.
if (!props.app.loaded) {
window.addEventListener("message", onIframeLoaded)
} else if (hasHiddenBar) {
postIframeMessage({ bind: "mousemove", prop: "clientY" })
window.removeEventListener("message", onIframeLoaded)
window.addEventListener("message", onHover)
}
} else if (hasHiddenBar) {
document.addEventListener("mousemove", onHover)
}
return (): void => {
document.removeEventListener("mousemove", onHover)
window.removeEventListener("message", onHover)
window.removeEventListener("message", onIframeLoaded)
if (props.app && !isExecutableApplication(props.app)) {
postIframeMessage({ unbind: "mousemove" })
}
if (timeout) {
clearTimeout(timeout)
}
}
}, [showBar, props.error, showModal, props.app])
const content = (): React.ReactElement => {
if (!props.authed) {
return <Login setApp={setApp} />
}
switch (section) {
case Section.Recent:
return <Recent app={props.app} setApp={setApp} />
case Section.Home:
return <Home app={props.app} />
case Section.Browse:
return <Browse />
case Section.Open:
return <Open app={props.app} setApp={setApp} />
default:
return <Missing />
}
}
return props.error || showModal || !props.app || !props.app.loaded ? (
<div className="modal-container">
<div className="modal">
{props.authed && (!props.app || props.app.loaded) ? (
<aside className="sidebar-nav">
<nav className="links">
{props.authed ? (
<>
<button className="link" onClick={(): void => setSection(Section.Recent)}>
Recent
</button>
<button className="link" onClick={(): void => setSection(Section.Open)}>
Open
</button>
<button className="link" onClick={(): void => setSection(Section.Browse)}>
Browse
</button>
</>
) : (
undefined
)}
</nav>
<div className="footer">
{props.app && props.app.loaded && !props.error ? (
<button className="close" onClick={(): void => setShowModal(false)}>
Close
</button>
) : (
undefined
)}
</div>
</aside>
) : (
undefined
)}
{props.error ? (
<RequestError
error={props.error}
onClose={(): void => {
props.setApp(undefined)
props.setError(undefined)
}}
/>
) : (
<div className="content">{content()}</div>
)}
</div>
</div>
) : (
<Animate show={showBar} delay={200}>
<div className="modal-bar">
<div className="bar">
<div className="content">
<div className="help">
Hover at the top {/*or press <strong>Ctrl+Shift+G</strong>*/} to display this menu.
</div>
</div>
<div className="open">
<button className="button" onClick={(): void => setShowModal(true)}>
Open Modal
</button>
</div>
<button className="close" onClick={(): void => setShowBar(false)}>
x
</button>
</div>
</div>
</Animate>
)
}

View File

@ -1,14 +0,0 @@
import * as React from "react"
import * as ReactDOM from "react-dom"
import App from "./app"
import { getOptions } from "../common/util"
import "./app.css"
import "./pages/home.css"
import "./pages/login.css"
import "./pages/missing.css"
import "./components/error.css"
import "./components/list.css"
import "./components/modal.css"
ReactDOM.hydrate(<App options={getOptions()} />, document.getElementById("root"))

24
src/browser/pages/app.css Normal file
View File

@ -0,0 +1,24 @@
/* NOTE: Disable scrollbars since an oversized element creates them. */
.app-input {
height: 100%;
left: 0;
outline: none;
position: fixed;
scrollbar-width: none;
top: 0;
width: 100%;
z-index: 20;
}
.app-input::-webkit-scrollbar {
display: none;
}
.app-render {
height: 100%;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 10;
}

View File

@ -3,17 +3,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="font-src 'self' fonts.gstatic.com; connect-src 'self'; default-src ws: wss: 'self'; style-src 'self' fonts.googleapis.com; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;">
<title>code-server</title>
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;">
<title>code-server — {{APP_NAME}}</title>
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans&display=swap" rel="stylesheet" />
<link href="{{BASE}}/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
<link href="{{BASE}}/static-{{COMMIT}}/dist/app.css" rel="stylesheet">
<meta id="coder-options" data-settings="{{OPTIONS}}">
</head>
<body>
<div id="root">{{COMPONENT}}</div>
<script src="{{BASE}}/static-{{COMMIT}}/dist/index.js"></script>
<script src="{{BASE}}/static-{{COMMIT}}/dist/app.js"></script>
</body>
</html>

14
src/browser/pages/app.ts Normal file
View File

@ -0,0 +1,14 @@
import { getOptions } from "../../common/util"
import "./app.css"
import "./error.css"
import "./global.css"
import "./home.css"
import "./login.css"
const options = getOptions()
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = options.base
const url = new URL(window.location.origin + "/" + parts.join("/"))
console.log(url)

View File

@ -1,33 +0,0 @@
import * as React from "react"
import { FilesResponse } from "../../common/api"
import { HttpError } from "../../common/http"
import { getFiles } from "../api"
import { RequestError } from "../components/error"
/**
* File browser.
*/
export const Browse: React.FunctionComponent = (props) => {
const [response, setResponse] = React.useState<FilesResponse>()
const [error, setError] = React.useState<HttpError>()
React.useEffect(() => {
getFiles()
.then(setResponse)
.catch(setError)
}, [props])
return (
<>
{error || (response && response.files.length === 0) ? (
<RequestError error={error || "Empty directory"} />
) : (
<ul>
{((response && response.files) || []).map((f, i) => (
<li key={i}>{f.name}</li>
))}
</ul>
)}
</>
)
}

View File

@ -0,0 +1,20 @@
.error-display {
box-sizing: border-box;
color: #fcfcfc;
padding: 20px;
text-align: center;
}
.error-display > .links {
margin-top: 1rem;
}
.error-display > .links > .link {
color: #b6b6b6;
text-decoration: none;
}
.error-display > .links > .link:hover {
color: #fcfcfc;
text-decoration: underline;
}

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;">
<title>code-server {{ERROR_TITLE}}</title>
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
<link href="{{BASE}}/static-{{COMMIT}}/dist/app.css" rel="stylesheet">
</head>
<body>
<div class="center-container">
<div class="error-display">
<h2 class="header">{{ERROR_HEADER}}</h2>
<div class="body">
{{ERROR_BODY}}
</div>
<div class="links">
<a class="link" href="{{BASE}}">go home</a
</div>
</div>
</div>
</body>
</html>

View File

@ -1,20 +1,16 @@
html,
body,
#root,
iframe {
.center-container {
height: 100%;
width: 100%;
}
iframe {
border: none;
}
body {
background: #272727;
color: #f4f4f4;
margin: 0;
font-family: 'IBM Plex Sans', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
overflow: hidden;
}
@ -22,20 +18,10 @@ button {
font-family: inherit;
}
.coder-splash {
.center-container {
align-items: center;
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 5;
}
.coder-splash > .logo {
color: rgba(255, 255, 255, 0.03);
width: 100%;
padding: 20px;
}

View File

@ -1,8 +1,70 @@
.orientation-guide {
align-items: center;
.app-lists {
max-width: 400px;
width: 100%;
}
.app-list > .header {
margin: 1rem 0;
}
.app-list > .none {
color: #b6b6b6;
}
.app-list + .app-list {
border-top: 1px solid #666;
margin-top: 1rem;
}
.app-row {
display: flex;
}
.app-row > .open {
color: #b6b6b6;
cursor: pointer;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
text-decoration: none;
}
.app-row > .open:hover {
color: #fafafa;
}
.app-row > .open > .icon {
height: 1rem;
margin-right: 5px;
width: 1rem;
}
.app-row > .open > .icon.-missing {
background-color: #eee;
color: #b6b6b6;
text-align: center;
}
.app-row > .open > .icon.-missing::after {
content: "?";
font-size: 0.7rem;
vertical-align: middle;
}
.kill-form {
display: inline-block;
}
.kill-form > .kill {
background-color: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
font-size: 1rem;
line-height: 1rem;
margin: 0;
padding: 0;
}
.kill-form > .kill:hover {
color: #fafafa;
}

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;">
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
<link href="{{BASE}}/static-{{COMMIT}}/dist/app.css" rel="stylesheet">
<meta id="coder-options" data-settings="{{OPTIONS}}">
</head>
<body>
<div class="center-container">
<div class="app-lists">
<div class="app-list">
<h2 class="header">Running Applications</h2>
{{APP_LIST:RUNNING}}
</div>
<div class="app-list">
<h2 class="header">Editors</h2>
{{APP_LIST:EDITORS}}
</div>
<div class="app-list">
<h2 class="header">Other</h2>
{{APP_LIST:OTHER}}
</div>
</div>
</div>
</body>
</html>

View File

@ -1,22 +0,0 @@
import * as React from "react"
import { Application } from "../../common/api"
import { authenticate, setAuthed } from "../api"
export interface HomeProps {
app?: Application
}
export const Home: React.FunctionComponent<HomeProps> = (props) => {
React.useEffect(() => {
authenticate()
.then(() => setAuthed(true))
.catch(() => undefined)
}, [])
return (
<div className="orientation-guide">
<div className="welcome">Welcome to code-server.</div>
{props.app && !props.app.loaded ? <div className="loader">loading...</div> : undefined}
</div>
)
}

View File

@ -1,10 +1,25 @@
.login-form {
align-items: center;
background: #fcfcfc;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.37);
display: flex;
flex: 1;
flex-direction: column;
flex: 1;
height: 100%;
justify-content: center;
max-height: 400px;
max-width: 664px;
padding: 20px;
position: relative;
width: 100%;
}
.login-form > .header {
align-items: center;
color: #b6b6b6;
margin-bottom: 1rem;
}
.login-form > .field {
@ -13,18 +28,19 @@
width: 100%;
}
.login-form > .field-error {
margin-top: 1em;
.login-form > .error {
color: red;
margin-top: 1rem;
}
.login-form > .field > .password {
border: 1px solid #b6b6b6;
box-sizing: border-box;
padding: 1em;
padding: 1rem;
flex: 1;
}
.login-form > .field > .user {
.login-form > .user {
display: none;
}
@ -33,11 +49,5 @@
border: 1px solid #b6b6b6;
box-sizing: border-box;
margin-left: -1px;
padding: 1em 2em;
}
.login-header {
align-items: center;
color: #b6b6b6;
margin-bottom: 1em;
padding: 1rem 2rem;
}

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; script-src 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;">
<title>code-server login</title>
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
<link href="{{BASE}}/static-{{COMMIT}}/dist/app.css" rel="stylesheet">
</head>
<body>
<div class="center-container">
<form class="login-form" method="post">
<div class="header">
<div class="main">Welcome to code-server</div>
<div class="sub">Please log in below</div>
</div>
<input class="user" type="text" autocomplete="username" />
<input id="base" type="hidden" name="base" value="/" />
<div class="field">
<!-- The onfocus code places the cursor at the end of the value. -->
<input
required autofocus
value="{{VALUE}}"
onfocus="const value=this.value;this.value='';this.value=value;"
class="password"
type="password"
placeholder="password"
name="password"
autocomplete="current-password"
/>
<button class="submit" type="submit">
Log In
</button>
</div>
{{ERROR}}
</form>
</div>
</body>
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
document.getElementById("base").value = url.pathname
</script>
</html>

View File

@ -1,59 +0,0 @@
import * as React from "react"
import { Application } from "../../common/api"
import { HttpError } from "../../common/http"
import { authenticate, setAuthed } from "../api"
import { FieldError } from "../components/error"
export interface LoginProps {
setApp(app: Application): void
}
/**
* Login page. Will redirect on success.
*/
export const Login: React.FunctionComponent<LoginProps> = (props) => {
const [password, setPassword] = React.useState<string>("")
const [error, setError] = React.useState<HttpError>()
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault()
authenticate({ password })
.then((response) => {
if (response.app) {
props.setApp(response.app)
}
setAuthed(true)
})
.catch(setError)
}
React.useEffect(() => {
authenticate()
.then(() => setAuthed(true))
.catch(() => undefined)
}, [])
return (
<form className="login-form" onSubmit={handleSubmit}>
<div className="login-header">
<div className="main">Welcome to code-server</div>
<div className="sub">Please log in below</div>
</div>
<div className="field">
<input className="user" type="text" autoComplete="username" />
<input
autoFocus
className="password"
type="password"
placeholder="password"
autoComplete="current-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => setPassword(event.target.value)}
/>
<button className="submit" type="submit">
Log In
</button>
</div>
{error ? <FieldError error={error} /> : undefined}
</form>
)
}

View File

@ -1,8 +0,0 @@
.missing-message {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}

View File

@ -1,9 +0,0 @@
import * as React from "react"
export const Missing: React.FunctionComponent = () => {
return (
<div className="missing-message">
<div className="message">404</div>
</div>
)
}

View File

@ -1,29 +0,0 @@
import * as React from "react"
import { Application } from "../../common/api"
import { getApplications } from "../api"
import { ApplicationSection, AppLoader } from "../components/list"
export interface OpenProps {
app?: Application
setApp(app: Application): void
}
/**
* Display recently used applications.
*/
export const Open: React.FunctionComponent<OpenProps> = (props) => {
return (
<AppLoader
getApps={async (): Promise<ReadonlyArray<ApplicationSection>> => {
const response = await getApplications()
return [
{
header: "Applications",
apps: response && response.applications,
},
]
}}
{...props}
/>
)
}

View File

@ -1,33 +0,0 @@
import * as React from "react"
import { Application } from "../../common/api"
import { getRecent } from "../api"
import { ApplicationSection, AppLoader } from "../components/list"
export interface RecentProps {
app?: Application
setApp(app: Application): void
}
/**
* Display recently used applications.
*/
export const Recent: React.FunctionComponent<RecentProps> = (props) => {
return (
<AppLoader
getApps={async (): Promise<ReadonlyArray<ApplicationSection>> => {
const response = await getRecent()
return [
{
header: "Running Applications",
apps: response && response.running,
},
{
header: "Recent Applications",
apps: response && response.recent,
},
]
}}
{...props}
/>
)
}

View File

@ -0,0 +1,92 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="font-src 'self'; connect-src 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data:;">
<!-- Disable pinch zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<!-- Workarounds/Hacks (remote user data uri) -->
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}">
<meta id="vscode-remote-commit" data-settings="{{COMMIT}}">
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}">
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<!-- PROD_ONLY
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{VS_BASE}}/static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
END_PROD_ONLY -->
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Prefetch to avoid waterfall -->
<!-- PROD_ONLY
<link rel="prefetch" href="{{VS_BASE}}/static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js">
END_PROD_ONLY -->
</head>
<body aria-label="">
</body>
<!-- Startup (do not modify order of script tags!) -->
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{VS_BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
const el = document.getElementById('vscode-remote-commit');
const commit = el ? el.getAttribute('data-settings') : "";
const staticBase = url.href + '/static-' + commit;
let nlsConfig;
try {
nlsConfig = JSON.parse(document.getElementById('vscode-remote-nls-configuration').getAttribute('data-settings'));
if (nlsConfig._resolvedLanguagePackCoreLocation) {
const bundles = Object.create(null);
nlsConfig.loadBundle = (bundle, language, cb) => {
let result = bundles[bundle];
if (result) {
return cb(undefined, result);
}
// FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation
+ '/' + bundle.replace(/\//g, '!') + '.nls.json';
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
bundles[bundle] = json;
cb(undefined, json);
})
.catch(cb);
};
}
} catch (error) { /* Probably fine. */ }
self.require = {
baseUrl: `${staticBase}/out`,
paths: {
'vscode-textmate': `${staticBase}/node_modules/vscode-textmate/release/main`,
'onigasm-umd': `${staticBase}/node_modules/onigasm-umd/release/main`,
'xterm': `${staticBase}/node_modules/xterm/lib/xterm.js`,
'xterm-addon-search': `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-web-links': `${staticBase}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
'xterm-addon-webgl': `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
'semver-umd': `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
},
'vs/nls': nlsConfig,
};
</script>
<script src="{{VS_BASE}}/static-{{COMMIT}}/out/vs/loader.js"></script>
<!-- PROD_ONLY
<script src="{{VS_BASE}}/static-{{COMMIT}}/out/vs/workbench/workbench.web.api.nls.js"></script>
<script src="{{VS_BASE}}/static-{{COMMIT}}/out/vs/workbench/workbench.web.api.js"></script>
END_PROD_ONLY -->
<script>
require(['vs/code/browser/workbench/workbench'], function() {});
</script>
</html>