diff --git a/codersdk/flags.go b/codersdk/flags.go index 09ca65b1ea813..bf407760bbfb8 100644 --- a/codersdk/flags.go +++ b/codersdk/flags.go @@ -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"` diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index ea078c17c65c3..c0759aecafdaf 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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 = () => { /> + + + + + + + + + } + /> + + + + + + + + } + /> + + + + + + + + } + /> + + + + + + + + } + /> + + }> } /> } /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index fb12571fd91ae..e9a0cd7e44cf8 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -641,3 +641,14 @@ export const getAgentListeningPorts = async ( ) return response.data } + +export const getDeploymentFlags = + async (): Promise => { + const response = await axios.get(`/api/v2/flags/deployment`) + return response.data + } + +export const getReplicas = async (): Promise => { + const response = await axios.get(`/api/v2/replicas`) + return response.data +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a4b2cf83a9581..d6305b3dbc8c0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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 diff --git a/site/src/components/DeploySettingsLayout/Badges.tsx b/site/src/components/DeploySettingsLayout/Badges.tsx new file mode 100644 index 0000000000000..7dd26009016ba --- /dev/null +++ b/site/src/components/DeploySettingsLayout/Badges.tsx @@ -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 ( + + Enabled + + ) +} + +export const DisabledBadge: React.FC = () => { + const styles = useStyles() + return ( + + Disabled + + ) +} + +export const EnterpriseBadge: React.FC = () => { + const styles = useStyles() + return ( + + Enterprise + + ) +} + +export const Badges: React.FC = ({ children }) => { + const styles = useStyles() + return ( + + {children} + + ) +} + +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, + }, +})) diff --git a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx new file mode 100644 index 0000000000000..b6abb27841f83 --- /dev/null +++ b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx @@ -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 = ({ + 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 ( + + + +
+ {deploymentFlags ? ( + + {children} + + ) : ( + + )} +
+
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + wrapper: { + padding: theme.spacing(6, 0), + }, + + content: { + maxWidth: 800, + width: "100%", + }, +})) diff --git a/site/src/components/DeploySettingsLayout/Header.tsx b/site/src/components/DeploySettingsLayout/Header.tsx new file mode 100644 index 0000000000000..ace8b79bf0b0d --- /dev/null +++ b/site/src/components/DeploySettingsLayout/Header.tsx @@ -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 ( + +
+

+ {title} +

+ {description} +
+ + {docsHref && ( + + )} +
+ ) +} + +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%", + }, +})) diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx new file mode 100644 index 0000000000000..3d59f0e056659 --- /dev/null +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -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 = ({ children }) => { + const styles = useStyles() + return {children} +} + +export const OptionDescription: React.FC = ({ + children, +}) => { + const styles = useStyles() + return {children} +} + +export const OptionValue: React.FC = ({ children }) => { + const styles = useStyles() + return {children} +} + +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), + }, + }, +})) diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx new file mode 100644 index 0000000000000..ad195acbdcd25 --- /dev/null +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -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 ( + + combineClasses([ + styles.sidebarNavItem, + isActive ? styles.sidebarNavItemActive : undefined, + ]) + } + > + + {icon} + {children} + + + ) +} + +const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({ + icon: Icon, +}) => { + const styles = useStyles() + return +} + +export const Sidebar: React.FC = () => { + const styles = useStyles() + + return ( + + ) +} + +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), + }, +})) diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index c9c51b6a7aebb..f9ffe05b545a2 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -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} /> ) } diff --git a/site/src/components/NavbarView/NavbarView.test.tsx b/site/src/components/NavbarView/NavbarView.test.tsx index 58ffd82766ef0..2b65a81b0026c 100644 --- a/site/src/components/NavbarView/NavbarView.test.tsx +++ b/site/src/components/NavbarView/NavbarView.test.tsx @@ -22,26 +22,54 @@ describe("NavbarView", () => { it("renders content", async () => { // When - render() + render( + , + ) // Then await screen.findAllByText("Coder", { exact: false }) }) it("workspaces nav link has the correct href", async () => { - render() + render( + , + ) const workspacesLink = await screen.findByText(navLanguage.workspaces) expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces") }) it("templates nav link has the correct href", async () => { - render() + render( + , + ) const templatesLink = await screen.findByText(navLanguage.templates) expect((templatesLink as HTMLAnchorElement).href).toContain("/templates") }) it("users nav link has the correct href", async () => { - render() + render( + , + ) const userLink = await screen.findByText(navLanguage.users) expect((userLink as HTMLAnchorElement).href).toContain("/users") }) @@ -55,7 +83,14 @@ describe("NavbarView", () => { } // When - render() + render( + , + ) // Then // There should be a 'B' avatar! @@ -64,16 +99,56 @@ describe("NavbarView", () => { }) it("audit nav link has the correct href", async () => { - render() + render( + , + ) const auditLink = await screen.findByText(navLanguage.audit) expect((auditLink as HTMLAnchorElement).href).toContain("/audit") }) it("audit nav link is hidden for members", async () => { render( - , + , ) const auditLink = screen.queryByText(navLanguage.audit) expect(auditLink).not.toBeInTheDocument() }) + + it("deployment nav link has the correct href", async () => { + render( + , + ) + 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( + , + ) + const auditLink = screen.queryByText(navLanguage.deployment) + expect(auditLink).not.toBeInTheDocument() + }) }) diff --git a/site/src/components/NavbarView/NavbarView.tsx b/site/src/components/NavbarView/NavbarView.tsx index 03a4451314c34..c7d2260344200 100644 --- a/site/src/components/NavbarView/NavbarView.tsx +++ b/site/src/components/NavbarView/NavbarView.tsx @@ -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< )} + {canViewDeployment && ( + + + {Language.deployment} + + + )} ) } @@ -72,6 +85,7 @@ export const NavbarView: React.FC> = ({ user, onSignOut, canViewAuditLog, + canViewDeployment, }) => { const styles = useStyles() const [isDrawerOpen, setIsDrawerOpen] = useState(false) @@ -98,7 +112,10 @@ export const NavbarView: React.FC> = ({
- + @@ -109,6 +126,7 @@ export const NavbarView: React.FC> = ({
@@ -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, diff --git a/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx new file mode 100644 index 0000000000000..066ccfe447e43 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx @@ -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 ( + <> + +
+
+ + + {deploymentFlags.oidc_client_id.value ? ( + + ) : ( + + )} + + + + + + + Option + Value + + + + + + + {deploymentFlags.oidc_client_id.name} + + + {deploymentFlags.oidc_client_id.description} + + + + + + {deploymentFlags.oidc_client_id.value} + + + + + + + + {deploymentFlags.oidc_client_secret.name} + + + {deploymentFlags.oidc_client_secret.description} + + + + + + {deploymentFlags.oidc_client_secret.value} + + + + + + + + {deploymentFlags.oidc_allow_signups.name} + + + {deploymentFlags.oidc_allow_signups.description} + + + + + + {deploymentFlags.oidc_allow_signups.value.toString()} + + + + + + + + {deploymentFlags.oidc_email_domain.name} + + + {deploymentFlags.oidc_email_domain.description} + + + + + + {deploymentFlags.oidc_email_domain.value} + + + + + + + + {deploymentFlags.oidc_issuer_url.name} + + + {deploymentFlags.oidc_issuer_url.description} + + + + + + {deploymentFlags.oidc_issuer_url.value} + + + + + + + {deploymentFlags.oidc_scopes.name} + + {deploymentFlags.oidc_scopes.description} + + + + + +
    + {deploymentFlags.oidc_scopes.value.map((scope) => ( +
  • {scope}
  • + ))} +
+
+
+
+
+
+
+
+ +
+
+ + + {deploymentFlags.oauth2_github_client_id.value ? ( + + ) : ( + + )} + + + + + + + Option + Value + + + + + + + {deploymentFlags.oauth2_github_client_id.name} + + + {deploymentFlags.oauth2_github_client_id.description} + + + + + + {deploymentFlags.oauth2_github_client_id.value} + + + + + + + + {deploymentFlags.oauth2_github_client_secret.name} + + + {deploymentFlags.oauth2_github_client_secret.description} + + + + + + {deploymentFlags.oauth2_github_client_secret.value} + + + + + + + + {deploymentFlags.oauth2_github_allow_signups.name} + + + {deploymentFlags.oauth2_github_allow_signups.description} + + + + + + {deploymentFlags.oauth2_github_allow_signups.value.toString()} + + + + + + + + {deploymentFlags.oauth2_github_allowed_organizations.name} + + + { + deploymentFlags.oauth2_github_allowed_organizations + .description + } + + + + + +
    + {deploymentFlags.oauth2_github_allowed_organizations.value.map( + (org) => ( +
  • {org}
  • + ), + )} +
+
+
+
+ + + + + {deploymentFlags.oauth2_github_allowed_teams.name} + + + {deploymentFlags.oauth2_github_allowed_teams.description} + + + + + +
    + {deploymentFlags.oauth2_github_allowed_teams.value.map( + (team) => ( +
  • {team}
  • + ), + )} +
+
+
+
+ + + + + {deploymentFlags.oauth2_github_enterprise_base_url.name} + + + { + deploymentFlags.oauth2_github_enterprise_base_url + .description + } + + + + + + {deploymentFlags.oauth2_github_enterprise_base_url.value} + + + +
+
+
+
+
+ + ) +} + +export default AuthSettingsPage diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx new file mode 100644 index 0000000000000..becc21b5eee6f --- /dev/null +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx @@ -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 ( + <> +
+ + + + + + Option + Value + + + + + + {deploymentFlags.access_url.name} + + {deploymentFlags.access_url.description} + + + + + {deploymentFlags.access_url.value} + + + + + + {deploymentFlags.address.name} + + {deploymentFlags.address.description} + + + + + {deploymentFlags.address.value} + + + + + + + {deploymentFlags.wildcard_access_url.name} + + + {deploymentFlags.wildcard_access_url.description} + + + + + + {deploymentFlags.wildcard_access_url.value} + + + + +
+
+ + ) +} + +export default GeneralSettingsPage diff --git a/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx b/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx new file mode 100644 index 0000000000000..ccdfbc605c460 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx @@ -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 ( + +
+
+ + + + + + Option + Value + + + + + + + {deploymentFlags.derp_server_enabled.name} + + + {deploymentFlags.derp_server_enabled.description} + + + + + + {deploymentFlags.derp_server_enabled.value ? ( + + ) : ( + + )} + + + + + + + + {deploymentFlags.derp_server_region_name.name} + + + {deploymentFlags.derp_server_region_name.description} + + + + + + {deploymentFlags.derp_server_region_name.value} + + + + + + + + {deploymentFlags.derp_server_stun_address.name} + + + {deploymentFlags.derp_server_stun_address.description} + + + + + + {deploymentFlags.derp_server_stun_address.value} + + + + + + + + {deploymentFlags.derp_config_url.name} + + + {deploymentFlags.derp_config_url.description} + + + + + + {deploymentFlags.derp_config_url.value} + + + + +
+
+
+
+ ) +} + +export default NetworkSettingsPage diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx new file mode 100644 index 0000000000000..6dfa81a8825a9 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx @@ -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 ( + +
+
+ + + + + + Option + Value + + + + + + + {deploymentFlags.ssh_keygen_algorithm.name} + + + {deploymentFlags.ssh_keygen_algorithm.description} + + + + + + {deploymentFlags.ssh_keygen_algorithm.value} + + + + + + + {deploymentFlags.secure_auth_cookie.name} + + + {deploymentFlags.secure_auth_cookie.description} + + + + + + {deploymentFlags.secure_auth_cookie.value ? ( + + ) : ( + + )} + + + + +
+
+
+ +
+
+ + + {entitlementsState.context.entitlements.features[ + FeatureNames.AuditLog + ].enabled ? ( + + ) : ( + + )} + + +
+ +
+
+ + + {entitlementsState.context.entitlements.features[ + FeatureNames.BrowserOnly + ].enabled ? ( + + ) : ( + + )} + + +
+ +
+
+ + + + + + Option + Value + + + + + + {deploymentFlags.tls_enable.name} + + {deploymentFlags.tls_enable.description} + + + + + + {deploymentFlags.tls_enable.value ? ( + + ) : ( + + )} + + + + + + + {deploymentFlags.tls_cert_files.name} + + {deploymentFlags.tls_cert_files.description} + + + + + +
    + {deploymentFlags.tls_cert_files.value.map( + (file, index) => ( +
  • {file}
  • + ), + )} +
+
+
+
+ + + + {deploymentFlags.tls_key_files.name} + + {deploymentFlags.tls_key_files.description} + + + + + +
    + {deploymentFlags.tls_key_files.value.map( + (file, index) => ( +
  • {file}
  • + ), + )} +
+
+
+
+ + + + + {deploymentFlags.tls_min_version.name} + + + {deploymentFlags.tls_min_version.description} + + + + + + {deploymentFlags.tls_min_version.value} + + + +
+
+
+
+
+ ) +} + +export default SecuritySettingsPage diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 78ac6fe4529bb..565ca3023a5b9 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -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 entitlementsXService: ActorRefFrom siteRolesXService: ActorRefFrom + // Since the info here is used by multiple deployment settings page and we don't want to refetch them every time + deploymentFlagsXService: ActorRefFrom } /** @@ -31,6 +34,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { buildInfoXService: useInterpret(buildInfoMachine), entitlementsXService: useInterpret(entitlementsMachine), siteRolesXService: useInterpret(siteRolesMachine), + deploymentFlagsXService: useInterpret(deploymentFlagsMachine), }} > {children} diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index d2cba576370b3..af25518f8bcaa 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -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: { diff --git a/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts b/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts new file mode 100644 index 0000000000000..249c49e2b1a79 --- /dev/null +++ b/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts @@ -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, + }), + }, + }, +)