Skip to content

Commit

Permalink
Create data model and transformers for Organization type (#9152)
Browse files Browse the repository at this point in the history
* create initial data model

* finish implementing transformers

* fix typing

* remove dead code

* switch back to LegacyUserRole and change organization.role to organization.isAdmin

* remove dead code; use UserRole

* remove the lifted value from Organzation

* rename OrganizationMembers -> OrganizationMemberships

* member -> membership
  • Loading branch information
grahamlangford committed Sep 13, 2024
1 parent 4652b33 commit 21e6b4e
Show file tree
Hide file tree
Showing 27 changed files with 410 additions and 178 deletions.
4 changes: 2 additions & 2 deletions src/auth/authTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { type UserPartner } from "@/data/model/UserPartner";
import { type PartnerPrincipal } from "@/data/model/PartnerPrincipal";
import { type OrganizationTheme } from "@/data/model/OrganizationTheme";
import { type ControlRoom } from "@/data/model/ControlRoom";
import { type UserRole } from "@/types/contract";
import { type UserMilestone } from "@/data/model/UserMilestone";
import { type LegacyUserRole } from "@/data/model/UserRole";

export type AuthSharing = "private" | "shared" | "built-in";

Expand Down Expand Up @@ -182,7 +182,7 @@ export type AuthUserOrganization = {
/**
* The user's role within the organization.
*/
role: UserRole;
role: LegacyUserRole;
/**
* The organization's brick scope, or null if not set.
*/
Expand Down
10 changes: 7 additions & 3 deletions src/auth/authUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ import {
} from "@/auth/authTypes";
import { type Me } from "@/data/model/Me";
import selectAuthUserOrganizations from "@/auth/selectAuthUserOrganizations";
import { UserRole } from "@/types/contract";
import {
readReduxStorage,
validateReduxStorageKey,
} from "@/utils/storageUtils";
import { type Nullishable } from "@/utils/nullishUtils";
import { anonAuth } from "@/auth/authConstants";
import { LegacyUserRole } from "@/data/model/UserRole";

const AUTH_SLICE_STORAGE_KEY = validateReduxStorageKey("persist:authOptions");

Expand Down Expand Up @@ -122,6 +122,10 @@ export function selectExtensionAuthState({
* Returns true if the role corresponds to permission to edit a package.
* See https://docs.pixiebrix.com/managing-teams/access-control/roles
*/
export function isPackageEditorRole(role: UserRole): boolean {
return [UserRole.admin, UserRole.manager, UserRole.developer].includes(role);
export function isPackageEditorRole(role: LegacyUserRole): boolean {
return [
LegacyUserRole.admin,
LegacyUserRole.manager,
LegacyUserRole.developer,
].includes(role);
}
4 changes: 2 additions & 2 deletions src/auth/selectAuthUserOrganizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { type AuthUserOrganization } from "@/auth/authTypes";
import { type Nullishable } from "@/utils/nullishUtils";
import { type MeOrganizationMembership } from "@/data/model/MeOrganizationMembership";
import { convertToLegacyUserRole } from "@/data/model/UserOrganizationMembershipRole";
import { convertToUserRole } from "@/data/model/UserRole";

// Export this function because it's used in both the Extension and the App
export default function selectAuthUserOrganizations(
Expand All @@ -40,7 +40,7 @@ export default function selectAuthUserOrganizations(
id: organizationId,
name: organizationName,
control_room: organizationControlRoom,
role: convertToLegacyUserRole(userOrganizationRole),
role: convertToUserRole(userOrganizationRole),
scope: organizationScope,
isDeploymentManager: meUserIsDeploymentManager,
}),
Expand Down
18 changes: 14 additions & 4 deletions src/components/fields/schemaFields/widgets/DatabaseCreateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import {
import SelectWidget from "@/components/form/widgets/SelectWidget";
import DatabaseGroupSelect from "@/components/fields/schemaFields/DatabaseGroupSelect";
import notify from "@/utils/notify";
import { type Organization, UserRole } from "@/types/contract";
import { type UUID } from "@/types/stringTypes";
import { validateUUID } from "@/types/helpers";
import { type Organization } from "@/data/model/Organization";
import { UserRole } from "@/data/model/UserRole";

type DatabaseCreateModalProps = {
show: boolean;
Expand Down Expand Up @@ -70,10 +71,19 @@ const initialValues: DatabaseConfig = {

function getOrganizationOptions(organizations: Organization[]) {
const organizationOptions = (organizations ?? [])
.filter((organization) => organization.role === UserRole.admin)
.filter(
(organization) =>
organization.memberships?.some(
(member) =>
// If the current user is an admin of the organization, then all of the members are listed for the organization
// Otherwise, only the current user is listed for the organization
// So if any listed member is an admin, the current user is an admin
member.role === UserRole.admin,
),
)
.map((organization) => ({
label: organization.name,
value: organization.id,
label: organization.organizationName,
value: organization.organizationId,
}));

const personalDbOption = {
Expand Down
8 changes: 4 additions & 4 deletions src/components/form/widgets/RegistryIdWidget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ import { render, screen } from "@/pageEditor/testHelpers";
import { authActions } from "@/auth/authSlice";
import userEvent from "@testing-library/user-event";
import { partition } from "lodash";
import { UserRole } from "@/types/contract";
import { LegacyUserRole } from "@/data/model/UserRole";
import { validateRegistryId } from "@/types/helpers";
import {
authStateFactory,
organizationStateFactory,
} from "@/testUtils/factories/authFactories";

const editorRoles = new Set<number>([
UserRole.admin,
UserRole.developer,
UserRole.manager,
LegacyUserRole.admin,
LegacyUserRole.developer,
LegacyUserRole.manager,
]);

describe("RegistryIdWidget", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/components/form/widgets/RegistryIdWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ import { type RegistryId } from "@/types/registryTypes";
import { Form } from "react-bootstrap";
import styles from "./RegistryIdWidget.module.scss";
import { type StylesConfig } from "react-select";
import { UserRole } from "@/types/contract";
import { LegacyUserRole } from "@/data/model/UserRole";

import { getScopeAndId } from "@/utils/registryUtils";
import useAsyncEffect from "use-async-effect";
import { assertNotNullish } from "@/utils/nullishUtils";

const editorRoles = new Set<number>([
UserRole.admin,
UserRole.developer,
UserRole.manager,
LegacyUserRole.admin,
LegacyUserRole.developer,
LegacyUserRole.manager,
]);

const emptyObject = {} as const;
Expand Down
12 changes: 5 additions & 7 deletions src/data/model/MeOrganizationMembership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

import { type UUID } from "@/types/stringTypes";
import {
transformUserOrganizationMembershipRoleResponse,
type UserOrganizationMembershipRole,
} from "@/data/model/UserOrganizationMembershipRole";
transformUserRoleResponse,
type UserRoleType,
} from "@/data/model/UserRole";
import {
type ControlRoom,
transformControlRoomResponse,
Expand All @@ -33,7 +33,7 @@ export type MeOrganizationMembership = {
*/
organizationId: UUID;
organizationName: string;
userOrganizationRole: UserOrganizationMembershipRole;
userOrganizationRole: UserRoleType;
/**
* Whether the (parent) user is a manager of one or more team deployments for the organization
*/
Expand All @@ -48,9 +48,7 @@ export function transformMeOrganizationMembershipResponse(
const membership: MeOrganizationMembership = {
organizationId: validateUUID(response.organization),
organizationName: response.organization_name,
userOrganizationRole: transformUserOrganizationMembershipRoleResponse(
response.role,
),
userOrganizationRole: transformUserRoleResponse(response.role),
meUserIsDeploymentManager: response.is_deployment_manager ?? false,
};

Expand Down
91 changes: 91 additions & 0 deletions src/data/model/Organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import {
transformOrganizationMembershipResponse,
type OrganizationMembership,
} from "@/data/model/OrganizationMemberships";
import {
transformOrganizationThemeResponse,
type OrganizationTheme,
} from "@/data/model/OrganizationTheme";
import {
type UserRoleType,
transformUserRoleResponse,
} from "@/data/model/UserRole";
import { validateUUID } from "@/types/helpers";
import { type Timestamp, type UUID } from "@/types/stringTypes";
import { type components } from "@/types/swagger";

export type Organization = {
/**
* The organization's ID.
*/
organizationId: UUID;
/**
* The organization's name.
*/
organizationName: string;
/**
* The organization's memberships.
*/
memberships: OrganizationMembership[] | null;
/**
* The organization's scope.
*/
scope: string | null;
/**
* The organization's default role.
*/
defaultRole: UserRoleType | null;
/**
* The organization's partner.
*/
partner: string | null;
/**
* The organization's enforce update millis.
*/
enforceUpdateMillis: number | null;
/**
* The organization's UI theme.
*/
theme: OrganizationTheme | null;

trialEndTimestamp: Timestamp | null;
};

export function transformOrganizationResponse(
baseQueryReturnValue: Array<components["schemas"]["Organization"]>,
): Organization[] {
return baseQueryReturnValue.map((apiOrganization) => ({
organizationId: validateUUID(apiOrganization.id),
organizationName: apiOrganization.name,
memberships: transformOrganizationMembershipResponse(
apiOrganization.members,
),
scope: apiOrganization.scope ?? null,
defaultRole: apiOrganization.default_role
? transformUserRoleResponse(apiOrganization.default_role)
: null,
partner: apiOrganization.partner ?? null,
enforceUpdateMillis: apiOrganization.enforce_update_millis ?? null,
theme: apiOrganization.theme
? transformOrganizationThemeResponse(apiOrganization.theme)
: null,
trialEndTimestamp: apiOrganization.trial_end_timestamp ?? null,
}));
}
39 changes: 39 additions & 0 deletions src/data/model/OrganizationMembershipGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type UUID } from "@/types/stringTypes";
import { type components } from "@/types/swagger";
import { type SetRequired } from "type-fest";

export type OrganizationMembershipGroup = {
groupId: UUID;
groupName: string;
};

type Memberships = SetRequired<
components["schemas"]["Organization"],
"members"
>["members"];

export function transformOrganizationMemberGroupsResponse(
groups: Memberships[number]["groups"],
): OrganizationMembershipGroup[] | undefined {
return groups?.map((group) => ({
groupId: group.id,
groupName: group.name,
}));
}
48 changes: 48 additions & 0 deletions src/data/model/OrganizationMembershipUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { validateUUID } from "@/types/helpers";
import { type Timestamp, type UUID } from "@/types/stringTypes";
import { type components } from "@/types/swagger";
import { type SetRequired } from "type-fest";

export type OrganizationMembershipUser = {
userId: UUID;
userName?: string;
userEmail?: string;
serviceAccount?: boolean;
deploymentKeyAccount?: boolean;
dateJoined?: Timestamp;
};

type Memberships = SetRequired<
components["schemas"]["Organization"],
"members"
>["members"];

export function transformOrganizationMemberUserResponse(
user: Memberships[number]["user"],
): OrganizationMembershipUser {
return {
userId: validateUUID(user?.id),
userName: user?.name,
userEmail: user?.email,
serviceAccount: user?.service_account,
deploymentKeyAccount: user?.deployment_key_account,
dateJoined: user?.date_joined,
};
}
Loading

0 comments on commit 21e6b4e

Please sign in to comment.