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 (
+