Skip to content

feat: create and modify organization groups #13887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 22, 2024
Prev Previous commit
Next Next commit
feedback
  • Loading branch information
aslilac committed Jul 19, 2024
commit c6360096986950e4e2580297ea31a060eb97b41e
9 changes: 5 additions & 4 deletions enterprise/coderd/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderd

import (
"database/sql"
"errors"
"fmt"
"net/http"

Expand Down Expand Up @@ -170,9 +171,9 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
OrganizationID: group.OrganizationID,
UserID: uuid.MustParse(id),
}))
if xerrors.Is(err, sql.ErrNoRows) {
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("User %q must be a member of organization %q", id, group.ID),
Message: fmt.Sprintf("User must be a member of organization %q", group.Name),
})
return
}
Expand Down Expand Up @@ -364,7 +365,7 @@ func (api *API) group(rw http.ResponseWriter, r *http.Request) {
)

users, err := api.Database.GetGroupMembersByGroupID(ctx, group.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
Expand All @@ -391,7 +392,7 @@ func (api *API) groups(rw http.ResponseWriter, r *http.Request) {
)

groups, err := api.Database.GetGroupsByOrganizationID(ctx, org.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
Expand Down
20 changes: 20 additions & 0 deletions site/src/components/PageHeader/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ export const PageHeaderCaption: FC<PropsWithChildren> = ({ children }) => {
</span>
);
};

interface ResourcePageHeaderProps extends Omit<PageHeaderProps, "children"> {
displayName?: string;
name: string;
}

export const ResourcePageHeader: FC<ResourcePageHeaderProps> = ({
displayName,
name,
...props
}) => {
const title = displayName || name;

return (
<PageHeader {...props}>
<PageHeaderTitle>{title}</PageHeaderTitle>
{name !== title && <PageHeaderSubtitle>{name}</PageHeaderSubtitle>}
</PageHeader>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mockApiError } from "testHelpers/entities";
import { CreateGroupPageView } from "./CreateGroupPageView";
import { userEvent, within } from "@storybook/test";

Check failure on line 4 in site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx

View workflow job for this annotation

GitHub Actions / lint

`@storybook/test` import should occur before import of `testHelpers/entities`

const meta: Meta<typeof CreateGroupPageView> = {
title: "pages/OrganizationGroupsPage/CreateGroupPageView",
Expand All @@ -18,6 +19,14 @@
message: "A group named new-group already exists.",
validations: [{ field: "name", detail: "Group names must be unique" }],
}),
initialTouched: { name: true },
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step("Enter name", async () => {
const input = canvas.getByLabelText("Name");
await userEvent.type(input, "new-group");
input.blur();
});
},
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import TextField from "@mui/material/TextField";
import { type FormikTouched, useFormik } from "formik";

Check failure on line 2 in site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx

View workflow job for this annotation

GitHub Actions / fmt

'FormikTouched' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 2 in site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx

View workflow job for this annotation

GitHub Actions / lint

'FormikTouched' is defined but never used. Allowed unused vars must match /^_/u
import type { FC } from "react";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
Expand All @@ -24,15 +24,12 @@
onSubmit: (data: CreateGroupRequest) => void;
error?: unknown;
isLoading: boolean;
// Helpful to show field errors on Storybook
initialTouched?: FormikTouched<CreateGroupRequest>;
};

export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
onSubmit,
error,
isLoading,
initialTouched,
}) => {
const navigate = useNavigate();
const form = useFormik<CreateGroupRequest>({
Expand All @@ -44,7 +41,6 @@
},
validationSchema,
onSubmit,
initialTouched,
});
const getFieldHelpers = getFormHelpers<CreateGroupRequest>(form, error);
const onCancel = () => navigate(-1);
Expand Down
18 changes: 5 additions & 13 deletions site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import {
PageHeader,

Check failure on line 42 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / fmt

'PageHeader' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 42 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeader' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 42 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeader' is defined but never used. Allowed unused vars must match /^_/u
PageHeaderSubtitle,

Check failure on line 43 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / fmt

'PageHeaderSubtitle' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 43 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeaderSubtitle' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 43 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeaderSubtitle' is defined but never used. Allowed unused vars must match /^_/u
PageHeaderTitle,

Check failure on line 44 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / fmt

'PageHeaderTitle' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 44 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeaderTitle' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 44 in site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'PageHeaderTitle' is defined but never used. Allowed unused vars must match /^_/u
ResourcePageHeader,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import {
Expand Down Expand Up @@ -103,7 +104,9 @@
{helmet}

<Margins>
<PageHeader
<ResourcePageHeader
displayName={groupData?.display_name}
name={groupData?.name}
actions={
canUpdateGroup && (
<>
Expand All @@ -127,18 +130,7 @@
</>
)
}
>
<PageHeaderTitle>
{groupData?.display_name || groupData?.name}
</PageHeaderTitle>
<PageHeaderSubtitle>
{/* Show the name if it differs from the display name. */}
{groupData?.display_name &&
groupData?.display_name !== groupData?.name
? groupData?.name
: ""}{" "}
</PageHeaderSubtitle>
</PageHeader>
/>

<Stack spacing={1}>
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
import { Loader } from "components/Loader/Loader";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { ResourcePageHeader } from "components/PageHeader/PageHeader";
import {
getFormHelpers,
nameValidator,
Expand Down Expand Up @@ -59,72 +59,65 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
const getFieldHelpers = getFormHelpers<FormData>(form, errors);

return (
<>
<PageHeader css={{ paddingTop: 8 }}>
<PageHeaderTitle>{group.name}</PageHeaderTitle>
</PageHeader>
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
title="Group settings"
description="Set a name and avatar for this group."
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
autoComplete="name"
autoFocus
fullWidth
label="Name"
disabled={isEveryoneGroup(group)}
/>
{!isEveryoneGroup(group) && (
<>
<TextField
{...getFieldHelpers("display_name", {
helperText: "Optional: keep empty to default to the name.",
})}
autoComplete="display_name"
autoFocus
fullWidth
label="Display Name"
disabled={isEveryoneGroup(group)}
/>
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
fullWidth
label="Avatar URL"
onPickEmoji={(value) =>
form.setFieldValue("avatar_url", value)
}
/>
</>
)}
</FormFields>
</FormSection>
<FormSection
title="Quota"
description="You can use quotas to restrict how many resources a user can create."
>
<FormFields>
<TextField
{...getFieldHelpers("quota_allowance", {
helperText: `This group gives ${form.values.quota_allowance} quota credits to each
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
title="Group settings"
description="Set a name and avatar for this group."
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
autoComplete="name"
autoFocus
fullWidth
label="Name"
disabled={isEveryoneGroup(group)}
/>
{!isEveryoneGroup(group) && (
<>
<TextField
{...getFieldHelpers("display_name", {
helperText: "Optional: keep empty to default to the name.",
})}
autoComplete="display_name"
autoFocus
fullWidth
label="Display Name"
disabled={isEveryoneGroup(group)}
/>
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
fullWidth
label="Avatar URL"
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
/>
</>
)}
</FormFields>
</FormSection>
<FormSection
title="Quota"
description="You can use quotas to restrict how many resources a user can create."
>
<FormFields>
<TextField
{...getFieldHelpers("quota_allowance", {
helperText: `This group gives ${form.values.quota_allowance} quota credits to each
of its members.`,
})}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
type="number"
label="Quota Allowance"
/>
</FormFields>
</FormSection>
})}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
type="number"
label="Quota Allowance"
/>
</FormFields>
</FormSection>

<FormFooter onCancel={onCancel} isLoading={isLoading} />
</HorizontalForm>
</>
<FormFooter onCancel={onCancel} isLoading={isLoading} />
</HorizontalForm>
);
};

Expand All @@ -150,13 +143,20 @@ const GroupSettingsPageView: FC<SettingsGroupPageViewProps> = ({
}

return (
<UpdateGroupForm
group={group!}
onCancel={onCancel}
errors={formErrors}
isLoading={isUpdating}
onSubmit={onSubmit}
/>
<>
<ResourcePageHeader
displayName={group!.display_name}
name={group!.name}
css={{ paddingTop: 8 }}
/>
<UpdateGroupForm
group={group!}
onCancel={onCancel}
errors={formErrors}
isLoading={isUpdating}
onSubmit={onSubmit}
/>
</>
);
};

Expand Down
Loading
Loading