diff --git a/site/src/components/EditRolesButton/EditRolesButton.tsx b/site/src/components/EditRolesButton/EditRolesButton.tsx index e49902936bee3..771ba95e7068e 100644 --- a/site/src/components/EditRolesButton/EditRolesButton.tsx +++ b/site/src/components/EditRolesButton/EditRolesButton.tsx @@ -37,7 +37,7 @@ const Option: React.FC<{ onChange(e.currentTarget.value) }} /> - + {name} {description} @@ -142,7 +142,7 @@ export const EditRolesButton: FC = ({
- + {t("member")} {t("roleDescription.member")} @@ -182,7 +182,7 @@ const useStyles = makeStyles((theme) => ({ padding: 0, "&:disabled": { - opacity: 0.5, + opacity: 0, }, }, options: { @@ -190,6 +190,7 @@ const useStyles = makeStyles((theme) => ({ }, option: { cursor: "pointer", + fontSize: 14, }, checkbox: { padding: 0, @@ -202,13 +203,15 @@ const useStyles = makeStyles((theme) => ({ }, }, optionDescription: { - fontSize: 12, + fontSize: 13, color: theme.palette.text.secondary, + lineHeight: "160%", }, footer: { padding: theme.spacing(3), backgroundColor: theme.palette.background.paper, borderTop: `1px solid ${theme.palette.divider}`, + fontSize: 14, }, userIcon: { width: theme.spacing(2.5), // Same as the checkbox diff --git a/site/src/components/UsersTable/UsersTable.stories.tsx b/site/src/components/UsersTable/UsersTable.stories.tsx index 536c39253fa44..a40d44e8611c3 100644 --- a/site/src/components/UsersTable/UsersTable.stories.tsx +++ b/site/src/components/UsersTable/UsersTable.stories.tsx @@ -1,73 +1,80 @@ -import { ComponentMeta, Story } from "@storybook/react" import { MockUser, MockUser2, MockAssignableSiteRoles, + MockAuthMethods, } from "testHelpers/entities" -import { UsersTable, UsersTableProps } from "./UsersTable" +import { UsersTable } from "./UsersTable" +import type { Meta, StoryObj } from "@storybook/react" -export default { +const meta: Meta = { title: "components/UsersTable", component: UsersTable, args: { isNonInitialPage: false, + authMethods: MockAuthMethods, }, -} as ComponentMeta +} -const Template: Story = (args) => +export default meta +type Story = StoryObj -export const Example = Template.bind({}) -Example.args = { - users: [MockUser, MockUser2], - roles: MockAssignableSiteRoles, - canEditUsers: false, +export const Example: Story = { + args: { + users: [MockUser, MockUser2], + roles: MockAssignableSiteRoles, + canEditUsers: false, + }, } -export const Editable = Template.bind({}) -Editable.args = { - users: [ - MockUser, - MockUser2, - { - ...MockUser, - username: "John Doe", - email: "john.doe@coder.com", - roles: [], - status: "dormant", - }, - { - ...MockUser, - username: "Roger Moore", - email: "roger.moore@coder.com", - roles: [], - status: "suspended", - }, - { - ...MockUser, - username: "OIDC User", - email: "oidc.user@coder.com", - roles: [], - status: "active", - login_type: "oidc", - }, - ], - roles: MockAssignableSiteRoles, - canEditUsers: true, - canViewActivity: true, +export const Editable: Story = { + args: { + users: [ + MockUser, + MockUser2, + { + ...MockUser, + username: "John Doe", + email: "john.doe@coder.com", + roles: [], + status: "dormant", + }, + { + ...MockUser, + username: "Roger Moore", + email: "roger.moore@coder.com", + roles: [], + status: "suspended", + }, + { + ...MockUser, + username: "OIDC User", + email: "oidc.user@coder.com", + roles: [], + status: "active", + login_type: "oidc", + }, + ], + roles: MockAssignableSiteRoles, + canEditUsers: true, + canViewActivity: true, + }, } -export const Empty = Template.bind({}) -Empty.args = { - users: [], - roles: MockAssignableSiteRoles, +export const Empty: Story = { + args: { + users: [], + roles: MockAssignableSiteRoles, + }, } -export const Loading = Template.bind({}) -Loading.args = { - users: [], - roles: MockAssignableSiteRoles, - isLoading: true, -} -Loading.parameters = { - chromatic: { pauseAnimationAtEnd: true }, +export const Loading: Story = { + args: { + users: [], + roles: MockAssignableSiteRoles, + isLoading: true, + }, + parameters: { + chromatic: { pauseAnimationAtEnd: true }, + }, } diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index d84386a2e968e..51edb7e23c2a4 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -38,6 +38,7 @@ export interface UsersTableProps { isNonInitialPage: boolean actorID: string oidcRoleSyncEnabled: boolean + authMethods?: TypesGen.AuthMethods } export const UsersTable: FC> = ({ @@ -57,6 +58,7 @@ export const UsersTable: FC> = ({ isNonInitialPage, actorID, oidcRoleSyncEnabled, + authMethods, }) => { return ( @@ -70,10 +72,8 @@ export const UsersTable: FC> = ({ - {Language.loginTypeLabel} - {Language.statusLabel} - {Language.lastSeenLabel} - + {Language.loginTypeLabel} + {Language.statusLabel} {/* 1% is a trick to make the table cell width fit the content */} {canEditUsers && } @@ -96,6 +96,7 @@ export const UsersTable: FC> = ({ isNonInitialPage={isNonInitialPage} actorID={actorID} oidcRoleSyncEnabled={oidcRoleSyncEnabled} + authMethods={authMethods} /> diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index 82e154a3d0d0b..73abfd639a273 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -1,9 +1,8 @@ -import Box from "@mui/material/Box" -import { makeStyles } from "@mui/styles" +import Box, { BoxProps } from "@mui/material/Box" +import { makeStyles, useTheme } from "@mui/styles" import TableCell from "@mui/material/TableCell" import TableRow from "@mui/material/TableRow" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { LastUsed } from "components/LastUsed/LastUsed" import { Pill } from "components/Pill/Pill" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -16,6 +15,16 @@ import { TableRowMenu } from "../TableRowMenu/TableRowMenu" import { EditRolesButton } from "components/EditRolesButton/EditRolesButton" import { Stack } from "components/Stack/Stack" import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges" +import dayjs from "dayjs" +import { SxProps, Theme } from "@mui/material/styles" +import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined" +import KeyOutlined from "@mui/icons-material/KeyOutlined" +import GitHub from "@mui/icons-material/GitHub" +import PasswordOutlined from "@mui/icons-material/PasswordOutlined" +import relativeTime from "dayjs/plugin/relativeTime" +import ShieldOutlined from "@mui/icons-material/ShieldOutlined" + +dayjs.extend(relativeTime) const isOwnerRole = (role: TypesGen.Role): boolean => { return role.name === "owner" @@ -31,6 +40,7 @@ const sortRoles = (roles: TypesGen.Role[]) => { interface UsersTableBodyProps { users?: TypesGen.User[] + authMethods?: TypesGen.AuthMethods roles?: TypesGen.AssignableRoles[] isUpdatingUserRoles?: boolean canEditUsers?: boolean @@ -58,6 +68,7 @@ export const UsersTableBody: FC< React.PropsWithChildren > = ({ users, + authMethods, roles, onSuspendUser, onDeleteUser, @@ -80,7 +91,7 @@ export const UsersTableBody: FC< return ( - + @@ -156,7 +167,10 @@ export const UsersTableBody: FC< -
{user.login_type}
+
- {user.status} - - - + {user.status} + + {canEditUsers && ( { + let displayName = value as string + let icon = <> + const iconStyles: SxProps = { width: 14, height: 14 } + + if (value === "password") { + displayName = "Password" + icon = + } else if (value === "none") { + displayName = "None" + icon = + } else if (value === "github") { + displayName = "GitHub" + icon = + } else if (value === "token") { + displayName = "Token" + icon = + } else if (value === "oidc") { + displayName = + authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText + icon = + authMethods.oidc.iconUrl === "" ? ( + + ) : ( + + ) + } + + return ( + + {icon} + {displayName} + + ) +} + +const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => { + const theme: Theme = useTheme() + const t = dayjs(value) + const now = dayjs() + + let message = t.fromNow() + let color = theme.palette.text.secondary + + if (t.isAfter(now.subtract(1, "hour"))) { + color = theme.palette.success.light + // Since the agent reports on a 10m interval, + // the last_used_at can be inaccurate when recent. + message = "Now" + } else if (t.isAfter(now.subtract(3, "day"))) { + color = theme.palette.text.secondary + } else if (t.isAfter(now.subtract(1, "month"))) { + color = theme.palette.warning.light + } else if (t.isAfter(now.subtract(100, "year"))) { + color = theme.palette.error.light + } else { + message = "Never" + } + + return ( + + {message} + + ) +} + const useStyles = makeStyles((theme) => ({ status: { textTransform: "capitalize", diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index ad3ccb25badb2..241f150650cef 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -20,6 +20,8 @@ import { useStatusFilterMenu } from "./UsersFilter" import { useFilter } from "components/Filter/filter" import { useDashboard } from "components/Dashboard/DashboardProvider" import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine" +import { useQuery } from "@tanstack/react-query" +import { getAuthMethods } from "api/api" export const Language = { suspendDialogTitle: "Suspend user", @@ -78,16 +80,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { const oidcRoleSyncEnabled = viewDeploymentValues && deploymentValues?.config.oidc?.user_role_field !== "" - - // Is loading if - // - users are loading or - // - the user can edit the users but the roles are loading - const isLoading = - usersState.matches("gettingUsers") || - (canEditUsers && rolesState.matches("gettingRoles")) - const me = useMe() - const useFilterResult = useFilter({ searchParamsResult, onUpdate: () => { @@ -105,6 +98,19 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { status: option?.value, }), }) + const authMethods = useQuery({ + queryKey: ["authMethods"], + queryFn: () => { + return getAuthMethods() + }, + }) + // Is loading if + // - users are loading or + // - the user can edit the users but the roles are loading + const isLoading = + usersState.matches("gettingUsers") || + (canEditUsers && rolesState.matches("gettingRoles")) || + authMethods.isLoading return ( <> @@ -115,6 +121,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { oidcRoleSyncEnabled={oidcRoleSyncEnabled} roles={roles} users={users} + authMethods={authMethods.data} count={count} onListWorkspaces={(user) => { navigate( diff --git a/site/src/pages/UsersPage/UsersPageView.stories.tsx b/site/src/pages/UsersPage/UsersPageView.stories.tsx index 1b860d2d4e29f..a08a758ad3dcd 100644 --- a/site/src/pages/UsersPage/UsersPageView.stories.tsx +++ b/site/src/pages/UsersPage/UsersPageView.stories.tsx @@ -5,6 +5,7 @@ import { MockUser2, MockAssignableSiteRoles, mockApiError, + MockAuthMethods, } from "testHelpers/entities" import { UsersPageView } from "./UsersPageView" import { ComponentProps } from "react" @@ -33,6 +34,7 @@ const meta: Meta = { count: 2, canEditUsers: true, filterProps: defaultFilterProps, + authMethods: MockAuthMethods, }, } diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index f11224eb609f8..13ba8ef9c4047 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -22,6 +22,7 @@ export interface UsersPageViewProps { oidcRoleSyncEnabled: boolean canViewActivity?: boolean isLoading?: boolean + authMethods?: TypesGen.AuthMethods onSuspendUser: (user: TypesGen.User) => void onDeleteUser: (user: TypesGen.User) => void onListWorkspaces: (user: TypesGen.User) => void @@ -58,6 +59,7 @@ export const UsersPageView: FC> = ({ paginationRef, isNonInitialPage, actorID, + authMethods, }) => { return ( <> @@ -89,6 +91,7 @@ export const UsersPageView: FC> = ({ isLoading={isLoading} isNonInitialPage={isNonInitialPage} actorID={actorID} + authMethods={authMethods} /> diff --git a/site/src/components/LastUsed/LastUsed.tsx b/site/src/pages/WorkspacesPage/LastUsed.tsx similarity index 100% rename from site/src/components/LastUsed/LastUsed.tsx rename to site/src/pages/WorkspacesPage/LastUsed.tsx diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 1e15f4c362d4f..9ebffb5cfc6da 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -27,7 +27,7 @@ import Box from "@mui/material/Box" import { AvatarData } from "components/AvatarData/AvatarData" import { Avatar } from "components/Avatar/Avatar" import { Stack } from "components/Stack/Stack" -import { LastUsed } from "components/LastUsed/LastUsed" +import { LastUsed } from "pages/WorkspacesPage/LastUsed" import { WorkspaceOutdatedTooltip } from "components/Tooltips" import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { getDisplayWorkspaceTemplateName } from "utils/workspace"