mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: Add preferences pages (#893)
This commit is contained in:
@ -1,12 +1,16 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Route, Routes } from "react-router-dom"
|
import { Navigate, Route, Routes } from "react-router-dom"
|
||||||
import { AuthAndNav, RequireAuth } from "./components"
|
import { AuthAndNav, RequireAuth } from "./components"
|
||||||
|
import { PreferencesLayout } from "./components/Preferences/Layout"
|
||||||
import { IndexPage } from "./pages"
|
import { IndexPage } from "./pages"
|
||||||
import { NotFoundPage } from "./pages/404"
|
import { NotFoundPage } from "./pages/404"
|
||||||
import { CliAuthenticationPage } from "./pages/cli-auth"
|
import { CliAuthenticationPage } from "./pages/cli-auth"
|
||||||
import { HealthzPage } from "./pages/healthz"
|
import { HealthzPage } from "./pages/healthz"
|
||||||
import { SignInPage } from "./pages/login"
|
import { SignInPage } from "./pages/login"
|
||||||
import { PreferencesPage } from "./pages/preferences"
|
import { PreferencesAccountPage } from "./pages/preferences/account"
|
||||||
|
import { PreferencesLinkedAccountsPage } from "./pages/preferences/linked-accounts"
|
||||||
|
import { PreferencesSecurityPage } from "./pages/preferences/security"
|
||||||
|
import { PreferencesSSHKeysPage } from "./pages/preferences/ssh-keys"
|
||||||
import { TemplatesPage } from "./pages/templates"
|
import { TemplatesPage } from "./pages/templates"
|
||||||
import { TemplatePage } from "./pages/templates/[organization]/[template]"
|
import { TemplatePage } from "./pages/templates/[organization]/[template]"
|
||||||
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
|
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
|
||||||
@ -68,15 +72,12 @@ export const AppRouter: React.FC = () => (
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="preferences">
|
<Route path="preferences" element={<PreferencesLayout />}>
|
||||||
<Route
|
<Route index element={<Navigate to="account" />} />
|
||||||
index
|
<Route path="account" element={<PreferencesAccountPage />} />
|
||||||
element={
|
<Route path="security" element={<PreferencesSecurityPage />} />
|
||||||
<AuthAndNav>
|
<Route path="ssh-keys" element={<PreferencesSSHKeysPage />} />
|
||||||
<PreferencesPage />
|
<Route path="linked-accounts" element={<PreferencesLinkedAccountsPage />} />
|
||||||
</AuthAndNav>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Using path="*"" means "match anything", so this route
|
{/* Using path="*"" means "match anything", so this route
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { Story } from "@storybook/react"
|
|
||||||
import React from "react"
|
|
||||||
import { Panel, PanelProps } from "./"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Page/Panel",
|
|
||||||
component: Panel,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Template: Story<PanelProps> = (args: PanelProps) => <Panel {...args} />
|
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
|
||||||
Example.args = {
|
|
||||||
title: "Panel title",
|
|
||||||
activeTab: "oauthSettings",
|
|
||||||
menuItems: [
|
|
||||||
{ label: "OAuth Settings", value: "oauthSettings" },
|
|
||||||
{ label: "Security", value: "oauthSettings", hasChanges: true },
|
|
||||||
{ label: "Hardware", value: "oauthSettings" },
|
|
||||||
],
|
|
||||||
}
|
|
34
site/src/components/Preferences/Layout.tsx
Normal file
34
site/src/components/Preferences/Layout.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Box from "@material-ui/core/Box"
|
||||||
|
import React from "react"
|
||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
import { AuthAndNav } from "../Page"
|
||||||
|
import { TabPanel } from "../TabPanel"
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
accountLabel: "Account",
|
||||||
|
securityLabel: "Security",
|
||||||
|
sshKeysLabel: "SSH Keys",
|
||||||
|
linkedAccountsLabel: "Linked Accounts",
|
||||||
|
preferencesLabel: "Preferences",
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ label: Language.accountLabel, path: "/preferences/account" },
|
||||||
|
{ label: Language.securityLabel, path: "/preferences/security" },
|
||||||
|
{ label: Language.sshKeysLabel, path: "/preferences/ssh-keys" },
|
||||||
|
{ label: Language.linkedAccountsLabel, path: "/preferences/linked-accounts" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const PreferencesLayout: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<AuthAndNav>
|
||||||
|
<Box display="flex" flexDirection="column">
|
||||||
|
<Box style={{ maxWidth: "1380px", margin: "1em auto" }}>
|
||||||
|
<TabPanel title={Language.preferencesLabel} menuItems={menuItems}>
|
||||||
|
<Outlet />
|
||||||
|
</TabPanel>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AuthAndNav>
|
||||||
|
)
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import { Story } from "@storybook/react"
|
|
||||||
import React from "react"
|
|
||||||
import { Sidebar, SidebarProps } from "./"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Page/Sidebar",
|
|
||||||
component: Sidebar,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Template: Story<SidebarProps> = (args: SidebarProps) => <Sidebar {...args} />
|
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
|
||||||
Example.args = {
|
|
||||||
activeItem: "oauthSettings",
|
|
||||||
menuItems: [
|
|
||||||
{ label: "OAuth Settings", value: "oauthSettings" },
|
|
||||||
{ label: "Security", value: "security", hasChanges: true },
|
|
||||||
{ label: "Hardware", value: "hardware" },
|
|
||||||
],
|
|
||||||
}
|
|
20
site/src/components/TabPanel/TabPanel.stories.tsx
Normal file
20
site/src/components/TabPanel/TabPanel.stories.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Story } from "@storybook/react"
|
||||||
|
import React from "react"
|
||||||
|
import { TabPanel, TabPanelProps } from "."
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "TabPanel/TabPanel",
|
||||||
|
component: TabPanel,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Template: Story<TabPanelProps> = (args: TabPanelProps) => <TabPanel {...args} />
|
||||||
|
|
||||||
|
export const Example = Template.bind({})
|
||||||
|
Example.args = {
|
||||||
|
title: "Title",
|
||||||
|
menuItems: [
|
||||||
|
{ label: "OAuth Settings", path: "oauthSettings" },
|
||||||
|
{ label: "Security", path: "oauthSettings", hasChanges: true },
|
||||||
|
{ label: "Hardware", path: "oauthSettings" },
|
||||||
|
],
|
||||||
|
}
|
19
site/src/components/TabPanel/TabSidebar.stories.tsx
Normal file
19
site/src/components/TabPanel/TabSidebar.stories.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Story } from "@storybook/react"
|
||||||
|
import React from "react"
|
||||||
|
import { TabSidebar, TabSidebarProps } from "./TabSidebar"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "TabPanel/TabSidebar",
|
||||||
|
component: TabSidebar,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Template: Story<TabSidebarProps> = (args: TabSidebarProps) => <TabSidebar {...args} />
|
||||||
|
|
||||||
|
export const Example = Template.bind({})
|
||||||
|
Example.args = {
|
||||||
|
menuItems: [
|
||||||
|
{ label: "OAuth Settings", path: "oauthSettings" },
|
||||||
|
{ label: "Security", path: "security", hasChanges: true },
|
||||||
|
{ label: "Hardware", path: "hardware" },
|
||||||
|
],
|
||||||
|
}
|
@ -2,41 +2,35 @@ import List from "@material-ui/core/List"
|
|||||||
import ListItem from "@material-ui/core/ListItem"
|
import ListItem from "@material-ui/core/ListItem"
|
||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { NavLink } from "react-router-dom"
|
||||||
import { combineClasses } from "../../util/combine-classes"
|
import { combineClasses } from "../../util/combine-classes"
|
||||||
|
|
||||||
export interface SidebarItem {
|
export interface TabSidebarItem {
|
||||||
value: string
|
path: string
|
||||||
label: string
|
label: string
|
||||||
hasChanges?: boolean
|
hasChanges?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface TabSidebarProps {
|
||||||
menuItems: SidebarItem[]
|
menuItems: TabSidebarItem[]
|
||||||
activeItem?: string
|
|
||||||
onSelect?: (value: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: React.FC<SidebarProps> = ({ menuItems, activeItem, onSelect }) => {
|
export const TabSidebar: React.FC<TabSidebarProps> = ({ menuItems }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className={styles.menu}>
|
<List className={styles.menu}>
|
||||||
{menuItems.map(({ hasChanges, ...tab }) => {
|
{menuItems.map(({ hasChanges, ...tab }) => {
|
||||||
const isActive = activeItem === tab.value
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<NavLink to={tab.path} key={tab.path} className={styles.link}>
|
||||||
key={tab.value}
|
{({ isActive }) => (
|
||||||
button
|
<ListItem button className={styles.menuItem} disableRipple focusRipple={false} component="li">
|
||||||
onClick={onSelect ? () => onSelect(tab.value) : undefined}
|
|
||||||
className={styles.menuItem}
|
|
||||||
disableRipple
|
|
||||||
focusRipple={false}
|
|
||||||
component="li"
|
|
||||||
>
|
|
||||||
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
|
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
|
||||||
{hasChanges ? `${tab.label}*` : tab.label}
|
{hasChanges ? `${tab.label}*` : tab.label}
|
||||||
</span>
|
</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
@ -49,6 +43,10 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginTop: theme.spacing(5),
|
marginTop: theme.spacing(5),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
link: {
|
||||||
|
textDecoration: "none",
|
||||||
|
},
|
||||||
|
|
||||||
menuItem: {
|
menuItem: {
|
||||||
letterSpacing: -theme.spacing(0.0375),
|
letterSpacing: -theme.spacing(0.0375),
|
||||||
padding: 0,
|
padding: 0,
|
@ -1,18 +1,16 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator"
|
import { fade } from "@material-ui/core/styles/colorManipulator"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Sidebar, SidebarItem } from "../Sidebar"
|
import { TabSidebar, TabSidebarItem } from "./TabSidebar"
|
||||||
|
|
||||||
export type AdminMenuItemCallback = (menuItem: string) => void
|
export type AdminMenuItemCallback = (menuItem: string) => void
|
||||||
|
|
||||||
export interface PanelProps {
|
export interface TabPanelProps {
|
||||||
title: string
|
title: string
|
||||||
menuItems: SidebarItem[]
|
menuItems: TabSidebarItem[]
|
||||||
activeTab: string
|
|
||||||
onSelect: AdminMenuItemCallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Panel: React.FC<PanelProps> = ({ children, title, menuItems, activeTab, onSelect }) => {
|
export const TabPanel: React.FC<TabPanelProps> = ({ children, title, menuItems }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -20,7 +18,7 @@ export const Panel: React.FC<PanelProps> = ({ children, title, menuItems, active
|
|||||||
<div className={styles.inner}>
|
<div className={styles.inner}>
|
||||||
<div className={styles.menuPanel}>
|
<div className={styles.menuPanel}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<Sidebar menuItems={menuItems} activeItem={activeTab} onSelect={onSelect} />
|
<TabSidebar menuItems={menuItems} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.contentPanel}>{children}</div>
|
<div className={styles.contentPanel}>{children}</div>
|
11
site/src/pages/preferences/account.tsx
Normal file
11
site/src/pages/preferences/account.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Section } from "../../components/Section"
|
||||||
|
|
||||||
|
const Language = {
|
||||||
|
title: "Account",
|
||||||
|
description: "Update your display name, email, profile picture, and dotfiles preferences.",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesAccountPage: React.FC = () => {
|
||||||
|
return <Section title={Language.title} description={Language.description} />
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import Box from "@material-ui/core/Box"
|
|
||||||
import Paper from "@material-ui/core/Paper"
|
|
||||||
import React from "react"
|
|
||||||
import { Header } from "../../components/Header"
|
|
||||||
import { Footer } from "../../components/Page"
|
|
||||||
|
|
||||||
export const PreferencesPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Box display="flex" flexDirection="column">
|
|
||||||
<Header title="Preferences" />
|
|
||||||
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>Preferences here!</Paper>
|
|
||||||
<Footer />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
12
site/src/pages/preferences/linked-accounts.tsx
Normal file
12
site/src/pages/preferences/linked-accounts.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Section } from "../../components/Section"
|
||||||
|
|
||||||
|
const Language = {
|
||||||
|
title: "Linked Accounts",
|
||||||
|
description:
|
||||||
|
"Linking your Coder account will add your workspace SSH key, allowing you to perform Git actions on all your workspaces.",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesLinkedAccountsPage: React.FC = () => {
|
||||||
|
return <Section title={Language.title} description={Language.description} />
|
||||||
|
}
|
11
site/src/pages/preferences/security.tsx
Normal file
11
site/src/pages/preferences/security.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Section } from "../../components/Section"
|
||||||
|
|
||||||
|
const Language = {
|
||||||
|
title: "Security",
|
||||||
|
description: "Changing your password will sign you out of your current session.",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesSecurityPage: React.FC = () => {
|
||||||
|
return <Section title={Language.title} description={Language.description} />
|
||||||
|
}
|
12
site/src/pages/preferences/ssh-keys.tsx
Normal file
12
site/src/pages/preferences/ssh-keys.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Section } from "../../components/Section"
|
||||||
|
|
||||||
|
const Language = {
|
||||||
|
title: "SSH Keys",
|
||||||
|
description:
|
||||||
|
"Coder automatically inserts a private key into every workspace; you can add the corresponding public key to any services (such as Git) that you need access to from your workspace.",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesSSHKeysPage: React.FC = () => {
|
||||||
|
return <Section title={Language.title} description={Language.description} />
|
||||||
|
}
|
Reference in New Issue
Block a user