diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index ea1cd167bf7ca..e2ddde14e2d00 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -1709,7 +1709,7 @@ func (q *FakeQuerier) GetGroupMembers(_ context.Context, id uuid.UUID) ([]databa for _, member := range members { for _, user := range q.users { - if user.ID == member.UserID && user.Status == database.UserStatusActive && !user.Deleted { + if user.ID == member.UserID && !user.Deleted { users = append(users, user) break } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 873ec9fdefed3..50da65e9c74a4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1276,8 +1276,6 @@ WHERE (group_members.group_id = $1 OR organization_members.organization_id = $1) -AND - users.status = 'active' AND users.deleted = 'false' ` diff --git a/coderd/database/queries/groupmembers.sql b/coderd/database/queries/groupmembers.sql index 0b3d0a33f4d54..4999df7930044 100644 --- a/coderd/database/queries/groupmembers.sql +++ b/coderd/database/queries/groupmembers.sql @@ -20,8 +20,6 @@ WHERE (group_members.group_id = @group_id OR organization_members.organization_id = @group_id) -AND - users.status = 'active' AND users.deleted = 'false'; diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 2f16aa7884934..bfbfffa5eecfb 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -648,7 +648,7 @@ func TestGroup(t *testing.T) { require.NotContains(t, group.Members, user1) }) - t.Run("FilterSuspendedUsers", func(t *testing.T) { + t.Run("IncludeSuspendedAndDormantUsers", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ @@ -679,8 +679,30 @@ func TestGroup(t *testing.T) { group, err = client.Group(ctx, group.ID) require.NoError(t, err) - require.Len(t, group.Members, 1) - require.NotContains(t, group.Members, user1) + require.Len(t, group.Members, 2) + require.Contains(t, group.Members, user1) + require.Contains(t, group.Members, user2) + + // cannot explicitly set a dormant user status so must create a new user + anotherUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "coder@coder.com", + Username: "coder", + Password: "SomeStrongPassword!", + OrganizationID: user.OrganizationID, + }) + require.NoError(t, err) + + // Ensure that new user has dormant account + require.Equal(t, codersdk.UserStatusDormant, anotherUser.Status) + + group, _ = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ + AddUsers: []string{anotherUser.ID.String()}, + }) + + group, err = client.Group(ctx, group.ID) + require.NoError(t, err) + require.Len(t, group.Members, 3) + require.Contains(t, group.Members, user1) require.Contains(t, group.Members, user2) }) diff --git a/site/src/components/LastSeen/LastSeen.tsx b/site/src/components/LastSeen/LastSeen.tsx new file mode 100644 index 0000000000000..df9c05210e5ea --- /dev/null +++ b/site/src/components/LastSeen/LastSeen.tsx @@ -0,0 +1,41 @@ +import Box, { type BoxProps } from "@mui/material/Box"; +import { useTheme } from "@emotion/react"; +import dayjs from "dayjs"; + +export const LastSeen = ({ + value, + ...boxProps +}: { value: string } & BoxProps) => { + const 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} + + ); +}; diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index bfc9a9824defc..e4113dc0d4b68 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -43,6 +43,9 @@ import { } from "api/queries/groups"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { getErrorMessage } from "api/errors"; +import Box from "@mui/material/Box"; +import { LastSeen } from "components/LastSeen/LastSeen"; +import { type Interpolation, type Theme } from "@emotion/react"; import LoadingButton from "@mui/lab/LoadingButton"; export const GroupPage: FC = () => { @@ -150,7 +153,8 @@ export const GroupPage: FC = () => { - User + User + Status @@ -259,7 +263,7 @@ const GroupMemberRow = (props: { return ( - + + + {member.status} + + {canUpdate && ( ({ }, })); +const styles = { + status: { + textTransform: "capitalize", + }, + suspended: (theme) => ({ + color: theme.palette.text.secondary, + }), +} satisfies Record>; + export default GroupPage; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 290a4f379d1bb..559fdc9c5ed93 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -1,8 +1,8 @@ -import Box, { type BoxProps } from "@mui/material/Box"; +import Box from "@mui/material/Box"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import Skeleton from "@mui/material/Skeleton"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import { type Interpolation, type Theme } from "@emotion/react"; import { type FC } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; @@ -22,6 +22,7 @@ import KeyOutlined from "@mui/icons-material/KeyOutlined"; import GitHub from "@mui/icons-material/GitHub"; import PasswordOutlined from "@mui/icons-material/PasswordOutlined"; import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import { LastSeen } from "components/LastSeen/LastSeen"; import { UserRoleCell } from "./UserRoleCell"; import { type GroupsByUserId } from "api/queries/groups"; import { UserGroupsCell } from "./UserGroupsCell"; @@ -273,41 +274,6 @@ const LoginType = ({ ); }; -const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => { - const 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 styles = { status: { textTransform: "capitalize",