Improve routing
This commit is contained in:
@ -12,7 +12,12 @@ iframe {
|
||||
|
||||
body {
|
||||
background: #272727;
|
||||
color: #f4f4f4;
|
||||
margin: 0;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
@ -1,35 +1,40 @@
|
||||
import { getBasepath, navigate } from "hookrouter"
|
||||
import * as React from "react"
|
||||
import { Application } from "../common/api"
|
||||
import { Route, Switch } from "react-router-dom"
|
||||
import { Application, isExecutableApplication } from "../common/api"
|
||||
import { HttpError } from "../common/http"
|
||||
import { normalize, Options } from "../common/util"
|
||||
import { Modal } from "./components/modal"
|
||||
import { getOptions } from "../common/util"
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const [authed, setAuthed] = React.useState<boolean>(false)
|
||||
const [app, setApp] = React.useState<Application>()
|
||||
export interface AppProps {
|
||||
options: Options
|
||||
}
|
||||
|
||||
const App: React.FunctionComponent<AppProps> = (props) => {
|
||||
const [authed, setAuthed] = React.useState<boolean>(!!props.options.authed)
|
||||
const [app, setApp] = React.useState<Application | undefined>(props.options.app)
|
||||
const [error, setError] = React.useState<HttpError | Error | string>()
|
||||
|
||||
React.useEffect(() => {
|
||||
getOptions()
|
||||
}, [])
|
||||
if (app && !isExecutableApplication(app)) {
|
||||
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
|
||||
}
|
||||
}, [app])
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(window as any).setAuthed = setAuthed
|
||||
;(window as any).setAuthed = (a: boolean): void => {
|
||||
if (authed !== a) {
|
||||
setAuthed(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/vscode" render={(): React.ReactElement => <iframe id="iframe" src="/vscode-embed"></iframe>} />
|
||||
<Route
|
||||
path="/"
|
||||
render={(): React.ReactElement => (
|
||||
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
|
||||
{authed && app && app.embedPath ? (
|
||||
<iframe id="iframe" src={normalize(`${getBasepath()}/${app.embedPath}/`, true)}></iframe>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -5,6 +5,10 @@ export interface DelayProps {
|
||||
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)
|
||||
|
@ -4,6 +4,7 @@ import { HttpError } from "../../common/http"
|
||||
export interface ErrorProps {
|
||||
error: HttpError | Error | string
|
||||
onClose?: () => void
|
||||
onCloseText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -15,7 +16,7 @@ export const RequestError: React.FunctionComponent<ErrorProps> = (props) => {
|
||||
<div className="error">{typeof props.error === "string" ? props.error : props.error.message}</div>
|
||||
{props.onClose ? (
|
||||
<button className="close" onClick={props.onClose}>
|
||||
Go Back
|
||||
{props.onCloseText || "Close"}
|
||||
</button>
|
||||
) : (
|
||||
undefined
|
||||
|
@ -20,7 +20,6 @@
|
||||
|
||||
.app-row {
|
||||
color: #b6b6b6;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
@ -106,3 +105,7 @@
|
||||
.app-list > .loader {
|
||||
color: #b6b6b6;
|
||||
}
|
||||
|
||||
.app-list > .app-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ 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 (
|
||||
<>
|
||||
@ -23,6 +26,9 @@ export interface AppRowProps {
|
||||
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>()
|
||||
@ -65,6 +71,11 @@ export interface AppListProps {
|
||||
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">
|
||||
@ -92,11 +103,12 @@ export interface AppLoaderProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display provided applications or sessions and allow opening them.
|
||||
* 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>()
|
||||
const [error, setError] = React.useState<HttpError | Error>()
|
||||
|
||||
const refresh = (): void => {
|
||||
props
|
||||
@ -105,33 +117,51 @@ export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
|
||||
.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(setError)
|
||||
.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) {
|
||||
props.setApp(undefined)
|
||||
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">
|
||||
@ -151,14 +181,16 @@ export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Apps are currently loading.
|
||||
if (!apps) {
|
||||
return (
|
||||
<div className="app-loader">
|
||||
<div className="loader">loading</div>
|
||||
<div className="loader">loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Apps have loaded.
|
||||
return (
|
||||
<>
|
||||
{apps.map((section, i) => (
|
||||
|
@ -100,19 +100,22 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar {
|
||||
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-width: 145px;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar > .links {
|
||||
.sidebar-nav > .links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar > .links > .link {
|
||||
.sidebar-nav > .links > .link {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.37);
|
||||
font-size: 1.4em;
|
||||
height: 31px;
|
||||
@ -122,7 +125,7 @@
|
||||
transition: 150ms color ease, 150ms height ease, 150ms margin-bottom ease;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar > .footer > .close {
|
||||
.sidebar-nav > .footer > .close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #b6b6b6;
|
||||
@ -130,14 +133,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar > .footer > .close:hover {
|
||||
.sidebar-nav > .links > .link[aria-current="page"],
|
||||
.sidebar-nav > .links > .link:hover,
|
||||
.sidebar-nav > .footer > .close:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .links > .link[aria-current="page"] {
|
||||
color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
.modal-container > .modal > .content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -145,3 +146,7 @@
|
||||
overflow: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.modal-container > .modal > .sidebar-nav {
|
||||
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as React from "react"
|
||||
import { NavLink, Route, RouteComponentProps, Switch } from "react-router-dom"
|
||||
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"
|
||||
@ -19,9 +19,18 @@ export interface ModalProps {
|
||||
setError(error?: HttpError | Error | string): void
|
||||
}
|
||||
|
||||
enum Section {
|
||||
Browse,
|
||||
Home,
|
||||
Login,
|
||||
Open,
|
||||
Recent,
|
||||
}
|
||||
|
||||
export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
||||
const [showModal, setShowModal] = React.useState<boolean>(false)
|
||||
const [showBar, setShowBar] = React.useState<boolean>(true)
|
||||
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)
|
||||
@ -36,7 +45,8 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
||||
clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
} else if (clientY <= 30 && !timeout) {
|
||||
timeout = setTimeout(() => setShowBar(true), 1000)
|
||||
// TEMP: No bar for now.
|
||||
// timeout = setTimeout(() => setShowBar(true), 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,39 +101,49 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
||||
}
|
||||
}, [showBar, props.error, showModal, props.app])
|
||||
|
||||
const content = (): React.ReactElement => {
|
||||
if (!props.authed) {
|
||||
return <Login />
|
||||
}
|
||||
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.Login:
|
||||
return <Login />
|
||||
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">
|
||||
<aside className="sidebar-nav">
|
||||
<nav className="links">
|
||||
{!props.authed ? (
|
||||
<NavLink className="link" to="/login">
|
||||
{props.authed ? (
|
||||
// TEMP: Remove once we don't auto-load vscode.
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
<button className="link" onClick={(): void => setSection(Section.Login)}>
|
||||
Login
|
||||
</NavLink>
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
{props.authed ? (
|
||||
<NavLink className="link" exact to="/recent/">
|
||||
Recent
|
||||
</NavLink>
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
{props.authed ? (
|
||||
<NavLink className="link" exact to="/open/">
|
||||
Open
|
||||
</NavLink>
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
{props.authed ? (
|
||||
<NavLink className="link" exact to="/browse/">
|
||||
Browse
|
||||
</NavLink>
|
||||
) : (
|
||||
undefined
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
<div className="footer">
|
||||
@ -148,23 +168,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="content">
|
||||
<Switch>
|
||||
<Route path="/login" component={Login} />
|
||||
<Route
|
||||
path="/recent"
|
||||
render={(p: RouteComponentProps): React.ReactElement => (
|
||||
<Recent app={props.app} setApp={setApp} {...p} />
|
||||
)}
|
||||
/>
|
||||
<Route path="/browse" component={Browse} />
|
||||
<Route
|
||||
path="/open"
|
||||
render={(p: RouteComponentProps): React.ReactElement => <Open app={props.app} setApp={setApp} {...p} />}
|
||||
/>
|
||||
<Route path="/" component={Home} />
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="content">{content()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +1,14 @@
|
||||
import * as React from "react"
|
||||
import * as ReactDOM from "react-dom"
|
||||
import App from "./app"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
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(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
document.getElementById("root")
|
||||
)
|
||||
ReactDOM.hydrate(<App options={getOptions()} />, document.getElementById("root"))
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as React from "react"
|
||||
import { RouteComponentProps } from "react-router"
|
||||
import { FilesResponse } from "../../common/api"
|
||||
import { HttpError } from "../../common/http"
|
||||
import { getFiles } from "../api"
|
||||
@ -8,14 +7,14 @@ import { RequestError } from "../components/error"
|
||||
/**
|
||||
* File browser.
|
||||
*/
|
||||
export const Browse: React.FunctionComponent<RouteComponentProps> = (props) => {
|
||||
export const Browse: React.FunctionComponent = (props) => {
|
||||
const [response, setResponse] = React.useState<FilesResponse>()
|
||||
const [error, setError] = React.useState<HttpError>()
|
||||
|
||||
React.useEffect(() => {
|
||||
getFiles()
|
||||
.then(setResponse)
|
||||
.catch((e) => setError(e.message))
|
||||
.catch(setError)
|
||||
}, [props])
|
||||
|
||||
return (
|
||||
|
@ -1,22 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { RouteComponentProps } from "react-router"
|
||||
import { Application } from "../../common/api"
|
||||
import { authenticate } from "../api"
|
||||
|
||||
export const Home: React.FunctionComponent<RouteComponentProps> = (props) => {
|
||||
export interface HomeProps {
|
||||
app?: Application
|
||||
}
|
||||
|
||||
export const Home: React.FunctionComponent<HomeProps> = (props) => {
|
||||
React.useEffect(() => {
|
||||
authenticate()
|
||||
.then(() => {
|
||||
// TEMP: Always redirect to VS Code.
|
||||
props.history.push("./vscode/")
|
||||
})
|
||||
.catch(() => {
|
||||
props.history.push("./login/")
|
||||
})
|
||||
authenticate().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>
|
||||
)
|
||||
}
|
||||
|
@ -4,9 +4,7 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
font-weight: 700;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.login-form > .field {
|
||||
@ -16,13 +14,13 @@
|
||||
}
|
||||
|
||||
.login-form > .field-error {
|
||||
margin-top: 10px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.login-form > .field > .input {
|
||||
border: 1px solid #b6b6b6;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
padding: 1em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -31,5 +29,11 @@
|
||||
border: 1px solid #b6b6b6;
|
||||
box-sizing: border-box;
|
||||
margin-left: -1px;
|
||||
padding: 10px 20px;
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
align-items: center;
|
||||
color: #b6b6b6;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as React from "react"
|
||||
import { RouteComponentProps } from "react-router"
|
||||
import { HttpError } from "../../common/http"
|
||||
import { authenticate } from "../api"
|
||||
import { FieldError } from "../components/error"
|
||||
@ -7,35 +6,25 @@ import { FieldError } from "../components/error"
|
||||
/**
|
||||
* Login page. Will redirect on success.
|
||||
*/
|
||||
export const Login: React.FunctionComponent<RouteComponentProps> = (props) => {
|
||||
export const Login: React.FunctionComponent = () => {
|
||||
const [password, setPassword] = React.useState<string>("")
|
||||
const [error, setError] = React.useState<HttpError>()
|
||||
|
||||
function redirect(): void {
|
||||
// TEMP: Always redirect to VS Code.
|
||||
console.log("is authed")
|
||||
props.history.push("../vscode/")
|
||||
// const params = new URLSearchParams(window.location.search)
|
||||
// props.history.push(params.get("to") || "/")
|
||||
}
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault()
|
||||
authenticate({ password })
|
||||
.then(redirect)
|
||||
.catch(setError)
|
||||
authenticate({ password }).catch(setError)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
authenticate()
|
||||
.then(redirect)
|
||||
.catch(() => {
|
||||
// Do nothing; we're already at the login page.
|
||||
})
|
||||
authenticate().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
|
||||
autoFocus
|
||||
|
8
src/browser/pages/missing.css
Normal file
8
src/browser/pages/missing.css
Normal file
@ -0,0 +1,8 @@
|
||||
.missing-message {
|
||||
align-items: center;
|
||||
color: #b6b6b6;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
9
src/browser/pages/missing.tsx
Normal file
9
src/browser/pages/missing.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import * as React from "react"
|
||||
|
||||
export const Missing: React.FunctionComponent = () => {
|
||||
return (
|
||||
<div className="missing-message">
|
||||
<div className="message">404</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user