mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: show dormant and suspended users in groups (#10333)
* fix: show dormant and suspended users in groups * added status column
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -1276,8 +1276,6 @@ WHERE
|
||||
(group_members.group_id = $1
|
||||
OR
|
||||
organization_members.organization_id = $1)
|
||||
AND
|
||||
users.status = 'active'
|
||||
AND
|
||||
users.deleted = 'false'
|
||||
`
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
component="span"
|
||||
data-chromatic="ignore"
|
||||
{...boxProps}
|
||||
sx={{ color, ...boxProps.sx }}
|
||||
>
|
||||
{message}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="99%">User</TableCell>
|
||||
<TableCell width="59%">User</TableCell>
|
||||
<TableCell width="40">Status</TableCell>
|
||||
<TableCell width="1%"></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -259,7 +263,7 @@ const GroupMemberRow = (props: {
|
||||
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell width="99%">
|
||||
<TableCell width="59%">
|
||||
<AvatarData
|
||||
avatar={
|
||||
<UserAvatar
|
||||
@@ -271,6 +275,13 @@ const GroupMemberRow = (props: {
|
||||
subtitle={member.email}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
width="40%"
|
||||
css={[styles.status, member.status === "suspended" && styles.suspended]}
|
||||
>
|
||||
<Box>{member.status}</Box>
|
||||
<LastSeen value={member.last_seen_at} sx={{ fontSize: 12 }} />
|
||||
</TableCell>
|
||||
<TableCell width="1%">
|
||||
{canUpdate && (
|
||||
<TableRowMenu
|
||||
@@ -313,4 +324,13 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const styles = {
|
||||
status: {
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
suspended: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default GroupPage;
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
component="span"
|
||||
data-chromatic="ignore"
|
||||
{...boxProps}
|
||||
sx={{ color, ...boxProps.sx }}
|
||||
>
|
||||
{message}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
status: {
|
||||
textTransform: "capitalize",
|
||||
|
||||
Reference in New Issue
Block a user