mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Add deployment settings page (#4590)
* Add base components for the Settings Page * WIP OIDC page * Imrove layout * Add table * Abstract option * Refactor badges * Load settings from the API * Update deployment page * feat: Add deployment settings page This allows deployment admins to view options set on their deployments. * Format * Remove replicas table since it's not used * Remove references to HA table * Fix tests * Improve language Co-authored-by: Bruno Quaresma <bruno@coder.com>
This commit is contained in:
@ -38,7 +38,7 @@ type DeploymentFlags struct {
|
||||
OAuth2GithubEnterpriseBaseURL *StringFlag `json:"oauth2_github_enterprise_base_url" typescript:",notnull"`
|
||||
OIDCAllowSignups *BoolFlag `json:"oidc_allow_signups" typescript:",notnull"`
|
||||
OIDCClientID *StringFlag `json:"oidc_client_id" typescript:",notnull"`
|
||||
OIDCClientSecret *StringFlag `json:"oidc_cliet_secret" typescript:",notnull"`
|
||||
OIDCClientSecret *StringFlag `json:"oidc_client_secret" typescript:",notnull"`
|
||||
OIDCEmailDomain *StringFlag `json:"oidc_email_domain" typescript:",notnull"`
|
||||
OIDCIssuerURL *StringFlag `json:"oidc_issuer_url" typescript:",notnull"`
|
||||
OIDCScopes *StringArrayFlag `json:"oidc_scopes" typescript:",notnull"`
|
||||
@ -49,7 +49,7 @@ type DeploymentFlags struct {
|
||||
TLSCertFiles *StringArrayFlag `json:"tls_cert_files" typescript:",notnull"`
|
||||
TLSClientCAFile *StringFlag `json:"tls_client_ca_file" typescript:",notnull"`
|
||||
TLSClientAuth *StringFlag `json:"tls_client_auth" typescript:",notnull"`
|
||||
TLSKeyFiles *StringArrayFlag `json:"tls_key_tiles" typescript:",notnull"`
|
||||
TLSKeyFiles *StringArrayFlag `json:"tls_key_files" typescript:",notnull"`
|
||||
TLSMinVersion *StringFlag `json:"tls_min_version" typescript:",notnull"`
|
||||
TraceEnable *BoolFlag `json:"trace_enable" typescript:",notnull"`
|
||||
SecureAuthCookie *BoolFlag `json:"secure_auth_cookie" typescript:",notnull"`
|
||||
|
@ -21,6 +21,7 @@ import { XServiceContext } from "xServices/StateContext"
|
||||
import { AuthAndFrame } from "./components/AuthAndFrame/AuthAndFrame"
|
||||
import { RequireAuth } from "./components/RequireAuth/RequireAuth"
|
||||
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
|
||||
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
|
||||
// Lazy load pages
|
||||
// - Pages that are secondary, not in the main navigation or not usually accessed
|
||||
@ -67,6 +68,18 @@ const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage"))
|
||||
const SettingsGroupPage = lazy(
|
||||
() => import("./pages/GroupsPage/SettingsGroupPage"),
|
||||
)
|
||||
const GeneralSettingsPage = lazy(
|
||||
() => import("./pages/DeploySettingsPage/GeneralSettingsPage"),
|
||||
)
|
||||
const SecuritySettingsPage = lazy(
|
||||
() => import("./pages/DeploySettingsPage/SecuritySettingsPage"),
|
||||
)
|
||||
const AuthSettingsPage = lazy(
|
||||
() => import("./pages/DeploySettingsPage/AuthSettingsPage"),
|
||||
)
|
||||
const NetworkSettingsPage = lazy(
|
||||
() => import("./pages/DeploySettingsPage/NetworkSettingsPage"),
|
||||
)
|
||||
|
||||
export const AppRouter: FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
@ -237,6 +250,65 @@ export const AppRouter: FC = () => {
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/settings/deployment">
|
||||
<Route
|
||||
path="general"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<RequirePermission
|
||||
isFeatureVisible={Boolean(permissions?.viewDeploymentFlags)}
|
||||
>
|
||||
<DeploySettingsLayout>
|
||||
<GeneralSettingsPage />
|
||||
</DeploySettingsLayout>
|
||||
</RequirePermission>
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="security"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<RequirePermission
|
||||
isFeatureVisible={Boolean(permissions?.viewDeploymentFlags)}
|
||||
>
|
||||
<DeploySettingsLayout>
|
||||
<SecuritySettingsPage />
|
||||
</DeploySettingsLayout>
|
||||
</RequirePermission>
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="network"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<RequirePermission
|
||||
isFeatureVisible={Boolean(permissions?.viewDeploymentFlags)}
|
||||
>
|
||||
<DeploySettingsLayout>
|
||||
<NetworkSettingsPage />
|
||||
</DeploySettingsLayout>
|
||||
</RequirePermission>
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="auth"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<RequirePermission
|
||||
isFeatureVisible={Boolean(permissions?.viewDeploymentFlags)}
|
||||
>
|
||||
<DeploySettingsLayout>
|
||||
<AuthSettingsPage />
|
||||
</DeploySettingsLayout>
|
||||
</RequirePermission>
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<SettingsLayout />}>
|
||||
<Route path="account" element={<AccountPage />} />
|
||||
<Route path="security" element={<SecurityPage />} />
|
||||
|
@ -641,3 +641,14 @@ export const getAgentListeningPorts = async (
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getDeploymentFlags =
|
||||
async (): Promise<TypesGen.DeploymentFlags> => {
|
||||
const response = await axios.get(`/api/v2/flags/deployment`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
|
||||
const response = await axios.get(`/api/v2/replicas`)
|
||||
return response.data
|
||||
}
|
||||
|
@ -293,7 +293,7 @@ export interface DeploymentFlags {
|
||||
readonly oauth2_github_enterprise_base_url: StringFlag
|
||||
readonly oidc_allow_signups: BoolFlag
|
||||
readonly oidc_client_id: StringFlag
|
||||
readonly oidc_cliet_secret: StringFlag
|
||||
readonly oidc_client_secret: StringFlag
|
||||
readonly oidc_email_domain: StringFlag
|
||||
readonly oidc_issuer_url: StringFlag
|
||||
readonly oidc_scopes: StringArrayFlag
|
||||
@ -304,7 +304,7 @@ export interface DeploymentFlags {
|
||||
readonly tls_cert_files: StringArrayFlag
|
||||
readonly tls_client_ca_file: StringFlag
|
||||
readonly tls_client_auth: StringFlag
|
||||
readonly tls_key_tiles: StringArrayFlag
|
||||
readonly tls_key_files: StringArrayFlag
|
||||
readonly tls_min_version: StringFlag
|
||||
readonly trace_enable: BoolFlag
|
||||
readonly secure_auth_cookie: BoolFlag
|
||||
|
79
site/src/components/DeploySettingsLayout/Badges.tsx
Normal file
79
site/src/components/DeploySettingsLayout/Badges.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React, { PropsWithChildren } from "react"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
|
||||
export const EnabledBadge: React.FC = () => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
||||
Enabled
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const DisabledBadge: React.FC = () => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<span className={combineClasses([styles.badge, styles.disabledBadge])}>
|
||||
Disabled
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const EnterpriseBadge: React.FC = () => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<span className={combineClasses([styles.badge, styles.enterpriseBadge])}>
|
||||
Enterprise
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const Badges: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Stack
|
||||
className={styles.badges}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
badges: {
|
||||
margin: theme.spacing(0, 0, 2),
|
||||
},
|
||||
|
||||
badge: {
|
||||
fontSize: 10,
|
||||
height: 24,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.085em",
|
||||
padding: theme.spacing(0, 1.5),
|
||||
borderRadius: 9999,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "fit-content",
|
||||
},
|
||||
|
||||
enterpriseBadge: {
|
||||
backgroundColor: theme.palette.info.dark,
|
||||
border: `1px solid ${theme.palette.info.light}`,
|
||||
},
|
||||
|
||||
enabledBadge: {
|
||||
border: `1px solid ${theme.palette.success.light}`,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
},
|
||||
|
||||
disabledBadge: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}))
|
@ -0,0 +1,73 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Margins } from "components/Margins/Margins"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { Sidebar } from "./Sidebar"
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from "react"
|
||||
import { useActor } from "@xstate/react"
|
||||
import { XServiceContext } from "xServices/StateContext"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { DeploymentFlags } from "api/typesGenerated"
|
||||
|
||||
type DeploySettingsContextValue = { deploymentFlags: DeploymentFlags }
|
||||
|
||||
const DeploySettingsContext = createContext<
|
||||
DeploySettingsContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export const useDeploySettings = (): DeploySettingsContextValue => {
|
||||
const context = useContext(DeploySettingsContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDeploySettings should be used inside of DeploySettingsLayout",
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const DeploySettingsLayout: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [state, send] = useActor(xServices.deploymentFlagsXService)
|
||||
const styles = useStyles()
|
||||
const { deploymentFlags } = state.context
|
||||
|
||||
useEffect(() => {
|
||||
if (state.matches("idle")) {
|
||||
send("LOAD")
|
||||
}
|
||||
}, [send, state])
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={5}>
|
||||
<Sidebar />
|
||||
<main className={styles.content}>
|
||||
{deploymentFlags ? (
|
||||
<DeploySettingsContext.Provider value={{ deploymentFlags }}>
|
||||
{children}
|
||||
</DeploySettingsContext.Provider>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</main>
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
wrapper: {
|
||||
padding: theme.spacing(6, 0),
|
||||
},
|
||||
|
||||
content: {
|
||||
maxWidth: 800,
|
||||
width: "100%",
|
||||
},
|
||||
}))
|
67
site/src/components/DeploySettingsLayout/Header.tsx
Normal file
67
site/src/components/DeploySettingsLayout/Header.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React from "react"
|
||||
|
||||
export const Header: React.FC<{
|
||||
title: string | JSX.Element
|
||||
description: string | JSX.Element
|
||||
secondary?: boolean
|
||||
docsHref?: string
|
||||
}> = ({ title, description, docsHref, secondary }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
|
||||
<div className={styles.headingGroup}>
|
||||
<h1 className={`${styles.title} ${secondary ? "secondary" : ""}`}>
|
||||
{title}
|
||||
</h1>
|
||||
<span className={styles.description}>{description}</span>
|
||||
</div>
|
||||
|
||||
{docsHref && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<LaunchOutlined />}
|
||||
component="a"
|
||||
href={docsHref}
|
||||
target="_blank"
|
||||
variant="outlined"
|
||||
>
|
||||
Read the docs
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
headingGroup: {
|
||||
maxWidth: 420,
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: "initial",
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
gap: theme.spacing(1),
|
||||
|
||||
"&.secondary": {
|
||||
fontSize: 24,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
},
|
||||
}))
|
40
site/src/components/DeploySettingsLayout/Option.tsx
Normal file
40
site/src/components/DeploySettingsLayout/Option.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import React, { PropsWithChildren } from "react"
|
||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
|
||||
|
||||
export const OptionName: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
return <span className={styles.optionName}>{children}</span>
|
||||
}
|
||||
|
||||
export const OptionDescription: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
return <span className={styles.optionDescription}>{children}</span>
|
||||
}
|
||||
|
||||
export const OptionValue: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
return <span className={styles.optionValue}>{children}</span>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
optionName: {
|
||||
display: "block",
|
||||
},
|
||||
optionDescription: {
|
||||
display: "block",
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 14,
|
||||
marginTop: theme.spacing(0.5),
|
||||
},
|
||||
optionValue: {
|
||||
fontSize: 14,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
|
||||
"& ul": {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
}))
|
114
site/src/components/DeploySettingsLayout/Sidebar.tsx
Normal file
114
site/src/components/DeploySettingsLayout/Sidebar.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
|
||||
import LockRounded from "@material-ui/icons/LockRounded"
|
||||
import Globe from "@material-ui/icons/Public"
|
||||
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React, { ElementType, PropsWithChildren, ReactNode } from "react"
|
||||
import { NavLink } from "react-router-dom"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
|
||||
const SidebarNavItem: React.FC<
|
||||
PropsWithChildren<{ href: string; icon: ReactNode }>
|
||||
> = ({ children, href, icon }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<NavLink
|
||||
to={href}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.sidebarNavItem,
|
||||
isActive ? styles.sidebarNavItemActive : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
<Stack alignItems="center" spacing={1.5} direction="row">
|
||||
{icon}
|
||||
{children}
|
||||
</Stack>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
|
||||
icon: Icon,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
return <Icon className={styles.sidebarNavItemIcon} />
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<nav className={styles.sidebar}>
|
||||
<SidebarNavItem
|
||||
href="../general"
|
||||
icon={<SidebarNavItemIcon icon={LaunchOutlined} />}
|
||||
>
|
||||
General
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem
|
||||
href="../auth"
|
||||
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
|
||||
>
|
||||
Authentication
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem
|
||||
href="../network"
|
||||
icon={<SidebarNavItemIcon icon={Globe} />}
|
||||
>
|
||||
Network
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem
|
||||
href="../security"
|
||||
icon={<SidebarNavItemIcon icon={LockRounded} />}
|
||||
>
|
||||
Security
|
||||
</SidebarNavItem>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
sidebar: {
|
||||
width: 245,
|
||||
},
|
||||
|
||||
sidebarNavItem: {
|
||||
color: "inherit",
|
||||
display: "block",
|
||||
fontSize: 16,
|
||||
textDecoration: "none",
|
||||
padding: theme.spacing(1.5, 1.5, 1.5, 3),
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
transition: "background-color 0.15s ease-in-out",
|
||||
marginBottom: 1,
|
||||
position: "relative",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
|
||||
sidebarNavItemActive: {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
|
||||
"&:before": {
|
||||
content: '""',
|
||||
display: "block",
|
||||
width: 3,
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
|
||||
sidebarNavItemIcon: {
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
},
|
||||
}))
|
@ -17,6 +17,7 @@ export const Navbar: React.FC = () => {
|
||||
const canViewAuditLog =
|
||||
featureVisibility[FeatureNames.AuditLog] &&
|
||||
Boolean(permissions?.viewAuditLog)
|
||||
const canViewDeployment = Boolean(permissions?.viewDeploymentFlags)
|
||||
const onSignOut = () => authSend("SIGN_OUT")
|
||||
|
||||
return (
|
||||
@ -24,6 +25,7 @@ export const Navbar: React.FC = () => {
|
||||
user={me}
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -22,26 +22,54 @@ describe("NavbarView", () => {
|
||||
|
||||
it("renders content", async () => {
|
||||
// When
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
|
||||
// Then
|
||||
await screen.findAllByText("Coder", { exact: false })
|
||||
})
|
||||
|
||||
it("workspaces nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const workspacesLink = await screen.findByText(navLanguage.workspaces)
|
||||
expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces")
|
||||
})
|
||||
|
||||
it("templates nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const templatesLink = await screen.findByText(navLanguage.templates)
|
||||
expect((templatesLink as HTMLAnchorElement).href).toContain("/templates")
|
||||
})
|
||||
|
||||
it("users nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const userLink = await screen.findByText(navLanguage.users)
|
||||
expect((userLink as HTMLAnchorElement).href).toContain("/users")
|
||||
})
|
||||
@ -55,7 +83,14 @@ describe("NavbarView", () => {
|
||||
}
|
||||
|
||||
// When
|
||||
render(<NavbarView user={mockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={mockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
|
||||
// Then
|
||||
// There should be a 'B' avatar!
|
||||
@ -64,16 +99,56 @@ describe("NavbarView", () => {
|
||||
})
|
||||
|
||||
it("audit nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const auditLink = await screen.findByText(navLanguage.audit)
|
||||
expect((auditLink as HTMLAnchorElement).href).toContain("/audit")
|
||||
})
|
||||
|
||||
it("audit nav link is hidden for members", async () => {
|
||||
render(
|
||||
<NavbarView user={MockUser2} onSignOut={noop} canViewAuditLog={false} />,
|
||||
<NavbarView
|
||||
user={MockUser2}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog={false}
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const auditLink = screen.queryByText(navLanguage.audit)
|
||||
expect(auditLink).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("deployment nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
canViewDeployment
|
||||
/>,
|
||||
)
|
||||
const auditLink = await screen.findByText(navLanguage.deployment)
|
||||
expect((auditLink as HTMLAnchorElement).href).toContain(
|
||||
"/settings/deployment/general",
|
||||
)
|
||||
})
|
||||
|
||||
it("deployment nav link is hidden for members", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
user={MockUser2}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog={false}
|
||||
canViewDeployment={false}
|
||||
/>,
|
||||
)
|
||||
const auditLink = screen.queryByText(navLanguage.deployment)
|
||||
expect(auditLink).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
@ -17,6 +17,7 @@ export interface NavbarViewProps {
|
||||
user?: TypesGen.User
|
||||
onSignOut: () => void
|
||||
canViewAuditLog: boolean
|
||||
canViewDeployment: boolean
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
@ -24,11 +25,16 @@ export const Language = {
|
||||
templates: "Templates",
|
||||
users: "Users",
|
||||
audit: "Audit",
|
||||
deployment: "Deployment",
|
||||
}
|
||||
|
||||
const NavItems: React.FC<
|
||||
React.PropsWithChildren<{ className?: string; canViewAuditLog: boolean }>
|
||||
> = ({ className, canViewAuditLog }) => {
|
||||
React.PropsWithChildren<{
|
||||
className?: string
|
||||
canViewAuditLog: boolean
|
||||
canViewDeployment: boolean
|
||||
}>
|
||||
> = ({ className, canViewAuditLog, canViewDeployment }) => {
|
||||
const styles = useStyles()
|
||||
const location = useLocation()
|
||||
|
||||
@ -65,6 +71,13 @@ const NavItems: React.FC<
|
||||
</NavLink>
|
||||
</ListItem>
|
||||
)}
|
||||
{canViewDeployment && (
|
||||
<ListItem button className={styles.item}>
|
||||
<NavLink className={styles.link} to="/settings/deployment/general">
|
||||
{Language.deployment}
|
||||
</NavLink>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
@ -72,6 +85,7 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
user,
|
||||
onSignOut,
|
||||
canViewAuditLog,
|
||||
canViewDeployment,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
@ -98,7 +112,10 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
<div className={styles.drawerHeader}>
|
||||
<Logo fill="white" opacity={1} width={125} />
|
||||
</div>
|
||||
<NavItems canViewAuditLog={canViewAuditLog} />
|
||||
<NavItems
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@ -109,6 +126,7 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
<NavItems
|
||||
className={styles.desktopNavItems}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
/>
|
||||
|
||||
<div className={styles.profileButton}>
|
||||
@ -192,7 +210,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontSize: 16,
|
||||
padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`,
|
||||
textDecoration: "none",
|
||||
transition: "background-color 0.3s ease",
|
||||
transition: "background-color 0.15s ease-in-out",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
|
314
site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx
Normal file
314
site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx
Normal file
@ -0,0 +1,314 @@
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import {
|
||||
Badges,
|
||||
DisabledBadge,
|
||||
EnabledBadge,
|
||||
} from "components/DeploySettingsLayout/Badges"
|
||||
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
import { Header } from "components/DeploySettingsLayout/Header"
|
||||
import {
|
||||
OptionDescription,
|
||||
OptionName,
|
||||
OptionValue,
|
||||
} from "components/DeploySettingsLayout/Option"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React from "react"
|
||||
|
||||
const AuthSettingsPage: React.FC = () => {
|
||||
const { deploymentFlags } = useDeploySettings()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" spacing={6}>
|
||||
<div>
|
||||
<Header
|
||||
title="Login with OpenID Connect"
|
||||
secondary
|
||||
description="Set up authentication to login with OpenID Connect."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/auth#openid-connect-with-google"
|
||||
/>
|
||||
|
||||
<Badges>
|
||||
{deploymentFlags.oidc_client_id.value ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
</Badges>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oidc_client_id.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_client_id.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oidc_client_id.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oidc_client_secret.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_client_secret.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oidc_client_secret.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oidc_allow_signups.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_allow_signups.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oidc_allow_signups.value.toString()}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oidc_email_domain.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_email_domain.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oidc_email_domain.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oidc_issuer_url.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_issuer_url.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oidc_issuer_url.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.oidc_scopes.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oidc_scopes.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
<ul>
|
||||
{deploymentFlags.oidc_scopes.value.map((scope) => (
|
||||
<li key={scope}>{scope}</li>
|
||||
))}
|
||||
</ul>
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Header
|
||||
title="Login with GitHub"
|
||||
secondary
|
||||
description="Set up authentication to login with GitHub."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/auth#github"
|
||||
/>
|
||||
|
||||
<Badges>
|
||||
{deploymentFlags.oauth2_github_client_id.value ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
</Badges>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_client_id.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oauth2_github_client_id.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oauth2_github_client_id.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_client_secret.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oauth2_github_client_secret.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oauth2_github_client_secret.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_allow_signups.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oauth2_github_allow_signups.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oauth2_github_allow_signups.value.toString()}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_allowed_organizations.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{
|
||||
deploymentFlags.oauth2_github_allowed_organizations
|
||||
.description
|
||||
}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
<ul>
|
||||
{deploymentFlags.oauth2_github_allowed_organizations.value.map(
|
||||
(org) => (
|
||||
<li key={org}>{org}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_allowed_teams.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.oauth2_github_allowed_teams.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
<ul>
|
||||
{deploymentFlags.oauth2_github_allowed_teams.value.map(
|
||||
(team) => (
|
||||
<li key={team}>{team}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.oauth2_github_enterprise_base_url.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{
|
||||
deploymentFlags.oauth2_github_enterprise_base_url
|
||||
.description
|
||||
}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.oauth2_github_enterprise_base_url.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthSettingsPage
|
85
site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx
Normal file
85
site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
import { Header } from "components/DeploySettingsLayout/Header"
|
||||
import {
|
||||
OptionDescription,
|
||||
OptionName,
|
||||
OptionValue,
|
||||
} from "components/DeploySettingsLayout/Option"
|
||||
import React from "react"
|
||||
|
||||
const GeneralSettingsPage: React.FC = () => {
|
||||
const { deploymentFlags } = useDeploySettings()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="General"
|
||||
description="Settings for accessing your Coder deployment."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/auth#openid-connect-with-google"
|
||||
/>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.access_url.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.access_url.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>{deploymentFlags.access_url.value}</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.address.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.address.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>{deploymentFlags.address.value}</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.wildcard_access_url.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.wildcard_access_url.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.wildcard_access_url.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralSettingsPage
|
121
site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx
Normal file
121
site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import {
|
||||
DisabledBadge,
|
||||
EnabledBadge,
|
||||
} from "components/DeploySettingsLayout/Badges"
|
||||
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
import { Header } from "components/DeploySettingsLayout/Header"
|
||||
import {
|
||||
OptionDescription,
|
||||
OptionName,
|
||||
OptionValue,
|
||||
} from "components/DeploySettingsLayout/Option"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React from "react"
|
||||
|
||||
const NetworkSettingsPage: React.FC = () => {
|
||||
const { deploymentFlags } = useDeploySettings()
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={6}>
|
||||
<div>
|
||||
<Header
|
||||
title="Network"
|
||||
description="Configure your deployment connectivity."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/auth#openid-connect-with-google"
|
||||
/>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.derp_server_enabled.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.derp_server_enabled.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.derp_server_enabled.value ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.derp_server_region_name.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.derp_server_region_name.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.derp_server_region_name.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.derp_server_stun_address.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.derp_server_stun_address.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.derp_server_stun_address.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.derp_config_url.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.derp_config_url.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.derp_config_url.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default NetworkSettingsPage
|
231
site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx
Normal file
231
site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import { useActor } from "@xstate/react"
|
||||
import { FeatureNames } from "api/types"
|
||||
import {
|
||||
Badges,
|
||||
DisabledBadge,
|
||||
EnabledBadge,
|
||||
EnterpriseBadge,
|
||||
} from "components/DeploySettingsLayout/Badges"
|
||||
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
import { Header } from "components/DeploySettingsLayout/Header"
|
||||
import {
|
||||
OptionDescription,
|
||||
OptionName,
|
||||
OptionValue,
|
||||
} from "components/DeploySettingsLayout/Option"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React, { useContext } from "react"
|
||||
import { XServiceContext } from "xServices/StateContext"
|
||||
|
||||
const SecuritySettingsPage: React.FC = () => {
|
||||
const { deploymentFlags } = useDeploySettings()
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [entitlementsState] = useActor(xServices.entitlementsXService)
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={6}>
|
||||
<div>
|
||||
<Header
|
||||
title="Security"
|
||||
description="Ensure your Coder deployment is secure."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/auth#openid-connect-with-google"
|
||||
/>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.ssh_keygen_algorithm.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.ssh_keygen_algorithm.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.ssh_keygen_algorithm.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.secure_auth_cookie.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.secure_auth_cookie.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.secure_auth_cookie.value ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Header
|
||||
title="Audit Logging"
|
||||
secondary
|
||||
description="Allow auditors to monitor user operations in your deployment."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/audit-logs"
|
||||
/>
|
||||
|
||||
<Badges>
|
||||
{entitlementsState.context.entitlements.features[
|
||||
FeatureNames.AuditLog
|
||||
].enabled ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
<EnterpriseBadge />
|
||||
</Badges>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Header
|
||||
title="Browser Only Connections"
|
||||
secondary
|
||||
description="Block all workspace access via SSH, port forward, and other non-browser connections."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/networking#browser-only-connections-enterprise"
|
||||
/>
|
||||
|
||||
<Badges>
|
||||
{entitlementsState.context.entitlements.features[
|
||||
FeatureNames.BrowserOnly
|
||||
].enabled ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
<EnterpriseBadge />
|
||||
</Badges>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Header
|
||||
title="TLS"
|
||||
secondary
|
||||
description="Ensure TLS is properly configured for your Coder deployment."
|
||||
/>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">Option</TableCell>
|
||||
<TableCell width="50%">Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.tls_enable.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.tls_enable.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.tls_enable.value ? (
|
||||
<EnabledBadge />
|
||||
) : (
|
||||
<DisabledBadge />
|
||||
)}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.tls_cert_files.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.tls_cert_files.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
<ul>
|
||||
{deploymentFlags.tls_cert_files.value.map(
|
||||
(file, index) => (
|
||||
<li key={index}>{file}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>{deploymentFlags.tls_key_files.name}</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.tls_key_files.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
<ul>
|
||||
{deploymentFlags.tls_key_files.value.map(
|
||||
(file, index) => (
|
||||
<li key={index}>{file}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<OptionName>
|
||||
{deploymentFlags.tls_min_version.name}
|
||||
</OptionName>
|
||||
<OptionDescription>
|
||||
{deploymentFlags.tls_min_version.description}
|
||||
</OptionDescription>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>
|
||||
{deploymentFlags.tls_min_version.value}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecuritySettingsPage
|
@ -3,6 +3,7 @@ import { createContext, FC, ReactNode } from "react"
|
||||
import { ActorRefFrom } from "xstate"
|
||||
import { authMachine } from "./auth/authXService"
|
||||
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
|
||||
import { deploymentFlagsMachine } from "./deploymentFlags/deploymentFlagsMachine"
|
||||
import { entitlementsMachine } from "./entitlements/entitlementsXService"
|
||||
import { siteRolesMachine } from "./roles/siteRolesXService"
|
||||
|
||||
@ -11,6 +12,8 @@ interface XServiceContextType {
|
||||
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
|
||||
entitlementsXService: ActorRefFrom<typeof entitlementsMachine>
|
||||
siteRolesXService: ActorRefFrom<typeof siteRolesMachine>
|
||||
// Since the info here is used by multiple deployment settings page and we don't want to refetch them every time
|
||||
deploymentFlagsXService: ActorRefFrom<typeof deploymentFlagsMachine>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,6 +34,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
buildInfoXService: useInterpret(buildInfoMachine),
|
||||
entitlementsXService: useInterpret(entitlementsMachine),
|
||||
siteRolesXService: useInterpret(siteRolesMachine),
|
||||
deploymentFlagsXService: useInterpret(deploymentFlagsMachine),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -16,6 +16,7 @@ export const checks = {
|
||||
createTemplates: "createTemplates",
|
||||
deleteTemplates: "deleteTemplates",
|
||||
viewAuditLog: "viewAuditLog",
|
||||
viewDeploymentFlags: "viewDeploymentFlags",
|
||||
createGroup: "createGroup",
|
||||
} as const
|
||||
|
||||
@ -56,6 +57,12 @@ export const permissionsToCheck = {
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewDeploymentFlags]: {
|
||||
object: {
|
||||
resource_type: "deployment_flags",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.createGroup]: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
@ -93,6 +100,7 @@ export type AuthEvent =
|
||||
| { type: "REGENERATE_SSH_KEY" }
|
||||
| { type: "CONFIRM_REGENERATE_SSH_KEY" }
|
||||
| { type: "CANCEL_REGENERATE_SSH_KEY" }
|
||||
| { type: "GET_AUTH_METHODS" }
|
||||
|
||||
export const authMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogBsATimEAzDIAcAFgUB2VTJ26ANCACeiAIwaADKsLKFtu-dsBfRwbRZc+IqQolyUBuS0ECJEvgBufADWXmTkAWL8gsKiSBKICubKhKpSyhoArAbGCGaqGoRSlVXVlfnOrhg4eATEsb7+gWAATl18XYQ8ADb4AGZ9ALatFPGpiSRCImKSCLLySmqa2ro6RaYFAEzWDscK9SBuTZ6EMOhCfgCqsN1BIYThUUQ3ALJgCQLzySW6Uy2VyBV2JXyUhMFRqcLqLnOjQ8LRudygj2e3V6-SGowm1zA6B+fySi1Sy32MnyhHyyksYK2ug0EJM1IURxO9jOFxRnyJ6IACt1xiRYKQRLAXpQ3uQItFCABjTBgRWRYVdUXi5LwWb-BYpUDLZRScygvKFIyIGQaQ4FHnI5r827tDVaiXkKXYvoDYboMaapUqtVusUe3W8fWAimIE1mnIW1l0rImOE1BENdxOwkuvw-LB8CBS4Iy94K75EzCFiMgOYGoEIZRUwgyVT5BQmfaW4omGxZGxcuwOrNXNHtfNVou0b24v0ByYVgtF0kA8lG2PN1vtzvd0zszmD06I3nZ7yUCABAa9EYkQahCB32j3QUAEQAggAVACibEFACUqAAMQYAAZL8V3rGMSjbGQKihLsISkOlaWHS4WjPSBLx4a9byIVAeAgfBXRwx8S1COUPkIE8rgwi9yCvPgbzvQh8MIoUSLABB3kVIiRAAbXMABdCDo3XaD8lgpCpAQq0EA0eTUL5KZzywjiWIIoi-EFDjpx6H08X9AlqPQ2JMPo7DGNw9S2OIyy7y4iieINAThL1MlDTScTJPg3dGxkfZFNPUy6OIWBMDeB8wFoJgvw-NhsGwAAJNgAGkvwATREtdPM7VQrE0XyTF7coB0PQKaOCy9xXCsc-ASxKUrAQxpXI+UiGMmIKDM0KaoFdp6sawwHIiJzkhcrKPOWIqkOscFZNTfYOXtY9HQqrqQuqnN0QGprdJxX18UDDrlO6zbaqgHahu43jyHGtzV0m0x9jyxQ5p7ExzBhUrB3Kkz1qqsLCEGPhkAgSAIsfP8vxilgvz-T8f3q1KMomht9g+8ocnMGQCtZDRZAPLkMyREc-pU+jNuB0HwcVEQb01S6-zAGBKC6TxaAAYTfFgOa-EC2ChmG4YR+KkuRzL7sgsT0bMQhzCUcxpMK20vsPBRieO2iAfCqmwYgJU6ZIBmksGpmWe6dmOaoFhgL-L4Behr9Yfh79ReStKJcjdy0Yx0Fsdx+b23MX7OvJnqgZBvXCC6ZmwFZzSLpN3ayNlNqqNWsnTsB3XwZj822e2pOrscm67q9h6GzMBQrDy7cZJ7DR1ZQlbSdDrOdcj3PY-jwuGt2mcDsMo6M7bjbs87-W87ji3e8G4a+FG-ihNRqCq5rtsO3r0wpG0ZvMzQ0eqtVVAunmQwIai5931d7Avw5+4-wYD9PdrKNsqmsoOQyBM3sQZ6pBDidDax9T7oHPqxBO2AQFnxaqnSimtKoU2gWA6ykDkHFxGqXZektRI5U-ooBkiZZIKFyHvEmB8gFH0VCfM+qDtroL2vpOcRkR6UKQdQ0B4CNL0I4Wfeei9brYPLlLPBjcCE-18m2KwGtWFa0CIwVgbAqD3A-CvaWWgYSEN-iUDIZpUxpiqBoQBZ52g0HQLAssoczFqM8k2WCW5N6+X2OYeShMTgyNbspUxdAB4GXnMpaxOD35-w0XLCRrJ0ZWH0QYqQRiW4UOVKqSI7RAJG1gOgTEXQLEUQVMdRJaoUlpIyU8Lo-CsGuWEbg5YmhCD7B3nU-YA5lA6HVhCZxYjoRshyDvJQ+NVCAIAO7IABH4QCfQPwqlSV0dJmT6DMHYJwGx1Sm7Yx0I3SwziTBOLqWaLQdIpC2HyKoLZ5gESInIIWOAYgEHrUCZU4JJRsY0iIT2akZoPEUJMX4GY9zHoIDbIcdGLzt4qFhDEpCgDzqZKWYgVQ+xWSyCyOC2okK+paRFGGHUML-mmnNNorZbIwUxI+Upc6E5qzYq2LUuQwKSitnymrI8+8lJyIYkxe8zELlfj0l0bFVcaRlCklvRspzjGILZVZEgkVCAzj5Y3AV+MfIQjyEy8hLLxUWXZRfOVRVsiKqVhCRuhxtgaBVY3A5MgxX-XMmpCB7E7K-CCX8oq+RnnaIKJa+J6rrUSrvHykwHZZq+X2bScwYbzUSTUBCr1QUfWbSlX6p1lctlusKp2Q4JLY1hzOmixOfdii-MrlIiotKPpyCtdm8e1N9YJsdYWqCmyshyH9vigoVhkWxIre3CO1aDbkHpuMRm3cZ51tft7BtqhzAZoVga+aGhexuOOJmtalaO69qnj3fqRc+UKHpIQIq87S25GXZnMea69Y7qhPuswxVCptnKNsOQ7Z2xUhPYfCmYV-WBtLVOjkJqzUkP6TGldp10EX0IFynlcroRywsI4iEu6ArAdPVQmhKDa0yqg0m1e+NNFwZ3BCNswdkPvuIGB2tcqakuPlgR4h2MWzMgAxartwDeEoLtf1dB-rXVBoQyQljqHOFfq+q2v9jHG7mqUKqm55N-UuN4-NT6DGdD5C0Gp-Ypq31eL8HcsdFcG0yA+rB5QtHijOKOYoNWgD8nJNGUU6F2GxK9LlvSTIcLGn5C2ZUNpLj5CxIUFSD6XmuyDOGeiMZXQJlgCmTMkp2LGm1OUFCHQORGkyEVqoZQbTNly2-poaERy6kh0pYl5LrZpLNIy1l2Sm5dAkPRs4wz+NlDOGcEAA */
|
||||
@ -340,6 +348,36 @@ export const authMachine =
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initial: "idle",
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
GET_AUTH_METHODS: {
|
||||
target: "gettingMethods",
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingMethods: {
|
||||
entry: "clearGetMethodsError",
|
||||
invoke: {
|
||||
src: "getMethods",
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignMethods", "clearGetMethodsError"],
|
||||
target: "idle",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
actions: "assignGetMethodsError",
|
||||
target: "idle",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: {
|
||||
initial: "idle",
|
||||
states: {
|
||||
|
61
site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts
Normal file
61
site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { getDeploymentFlags } from "api/api"
|
||||
import { DeploymentFlags } from "api/typesGenerated"
|
||||
import { createMachine, assign } from "xstate"
|
||||
|
||||
export const deploymentFlagsMachine = createMachine(
|
||||
{
|
||||
id: "deploymentFlagsMachine",
|
||||
initial: "idle",
|
||||
schema: {
|
||||
context: {} as {
|
||||
deploymentFlags?: DeploymentFlags
|
||||
getDeploymentFlagsError?: unknown
|
||||
},
|
||||
events: {} as { type: "LOAD" },
|
||||
services: {} as {
|
||||
getDeploymentFlags: {
|
||||
data: DeploymentFlags
|
||||
}
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./deploymentFlagsMachine.typegen").Typegen0,
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
LOAD: {
|
||||
target: "loading",
|
||||
},
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
invoke: {
|
||||
src: "getDeploymentFlags",
|
||||
onDone: {
|
||||
target: "loaded",
|
||||
actions: ["assignDeploymentFlags"],
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignGetDeploymentFlagsError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
loaded: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
services: {
|
||||
getDeploymentFlags,
|
||||
},
|
||||
actions: {
|
||||
assignDeploymentFlags: assign({
|
||||
deploymentFlags: (_, { data }) => data,
|
||||
}),
|
||||
assignGetDeploymentFlagsError: assign({
|
||||
getDeploymentFlagsError: (_, { data }) => data,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
Reference in New Issue
Block a user