Skip to content

Commit

Permalink
🪟 🔧 Associate newly created oss workspaces with organization (#8857)
Browse files Browse the repository at this point in the history
  • Loading branch information
teallarson committed Sep 29, 2023
1 parent 73ed4c6 commit efe23db
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 49 deletions.
12 changes: 11 additions & 1 deletion airbyte-webapp/src/core/api/hooks/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { useSuspenseQuery } from "../useSuspenseQuery";

export const organizationKeys = {
all: [SCOPE_USER, "organizations"] as const,
lists: () => [...organizationKeys.all, "list"] as const,
list: (filters: string[]) => [...organizationKeys.lists(), filters] as const,
detail: (organizationId: string) => [...organizationKeys.all, "details", organizationId] as const,
allListUsers: [SCOPE_ORGANIZATION, "users", "list"] as const,
listUsers: (organizationId: string) => [SCOPE_ORGANIZATION, "users", "list", organizationId] as const,
workspaces: (organizationIds: string[]) => [...organizationKeys.all, "workspaces", organizationIds] as const,
};

export const useOrganization = (organizationId: string) => {
Expand All @@ -36,6 +37,15 @@ export const useUpdateOrganization = () => {
);
};

export const useListOrganizationsById = (organizationIds: string[]) => {
const requestOptions = useRequestOptions();
const queryKey = organizationKeys.list(organizationIds);

return useSuspenseQuery(queryKey, () =>
Promise.all(organizationIds.map((organizationId) => getOrganization({ organizationId }, requestOptions)))
);
};

export const useListUsersInOrganization = (organizationId: string) => {
const requestOptions = useRequestOptions();
const queryKey = organizationKeys.listUsers(organizationId);
Expand Down
10 changes: 8 additions & 2 deletions airbyte-webapp/src/core/api/hooks/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import {
updateWorkspaceName,
webBackendGetWorkspaceState,
} from "../generated/AirbyteClient";
import { WorkspaceRead, WorkspaceReadList, WorkspaceUpdate, WorkspaceUpdateName } from "../types/AirbyteClient";
import {
WorkspaceCreate,
WorkspaceRead,
WorkspaceReadList,
WorkspaceUpdate,
WorkspaceUpdateName,
} from "../types/AirbyteClient";
import { useRequestOptions } from "../useRequestOptions";
import { useSuspenseQuery } from "../useSuspenseQuery";

Expand Down Expand Up @@ -44,7 +50,7 @@ export const useCreateWorkspace = () => {
const requestOptions = useRequestOptions();
const queryClient = useQueryClient();

return useMutation(async (name: string) => createWorkspace({ name }, requestOptions), {
return useMutation(async (workspaceCreate: WorkspaceCreate) => createWorkspace(workspaceCreate, requestOptions), {
onSuccess: (result) => {
queryClient.setQueryData<WorkspaceReadList>(workspaceKeys.lists(), (old) => ({
workspaces: [result, ...(old?.workspaces ?? [])],
Expand Down
4 changes: 3 additions & 1 deletion airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
"workspaces.title": "Workspaces",
"workspaces.subtitle": "Workspaces let you collaborate with team members and share connections across your team.",
"workspaces.create": "Create workspace",
"workspaces.create.organizationDescription": "This workspace will be created in the organization <b>{organizationName}</b>",
"workspaces.createSuccess": "Workspace created successfully",
"workspaces.createError": "Something went wrong while creating the workspace. Please try again.",
"workspaces.createNew": "New workspace",
"workspaces.createFirst": "Create your first workspace",
"workspaces.loading": "Loading…",
"workspaces.noWorkspaces": "No workspaces found",
"workspaces.workspace": "Workspace",
"workspaces.seeAll": "See all workspaces",
"workspaces.name": "Workspace name",

"user.roleLabel": "Role: {role}",
"role.admin": "Admin",
Expand Down Expand Up @@ -53,6 +54,7 @@
"form.email.placeholder": "[email protected]",
"form.organizationName": "Organization name",
"form.organizationName.placeholder": "ACME Corp",
"form.workspaceName": "Workspace name",
"form.email.error": "Enter a valid email",
"form.empty.error": "Required",
"form.selectValue": "Select a value",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { UseMutateAsyncFunction } from "@tanstack/react-query";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { useToggle } from "react-use";
import * as yup from "yup";

import { Form, FormControl } from "components/forms";
import { FormSubmissionButtons } from "components/forms/FormSubmissionButtons";
import { Box } from "components/ui/Box";
import { Button } from "components/ui/Button";
import { Card } from "components/ui/Card";
import { Icon } from "components/ui/Icon";

import { useListWorkspaces } from "core/api";
import { CloudWorkspaceRead } from "core/api/types/CloudApi";
import { trackError } from "core/utils/datadog";
import { useNotificationService } from "hooks/services/Notification";
import styles from "pages/workspaces/components/WorkspacesCreateControl.module.scss";

interface CreateCloudWorkspaceFormValues {
name: string;
}

const CreateCloudWorkspaceFormValidationSchema = yup.object().shape({
name: yup.string().trim().required("form.empty.error"),
});

interface CloudWorkspacesCreateControlProps {
createWorkspace: UseMutateAsyncFunction<CloudWorkspaceRead, unknown, string, unknown>;
}

export const CloudWorkspacesCreateControl: React.FC<CloudWorkspacesCreateControlProps> = ({ createWorkspace }) => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [isEditMode, toggleMode] = useToggle(false);
const { registerNotification } = useNotificationService();
const { workspaces } = useListWorkspaces();
const isFirstWorkspace = workspaces.length === 0;

const onSubmit = async (values: CreateCloudWorkspaceFormValues) => {
const newWorkspace = await createWorkspace(values.name);
toggleMode();
navigate(`/workspaces/${newWorkspace.workspaceId}`);
};

const onSuccess = () => {
registerNotification({
id: "workspaces.createSuccess",
text: formatMessage({ id: "workspaces.createSuccess" }),
type: "success",
});
};

const onError = (e: Error, { name }: CreateCloudWorkspaceFormValues) => {
trackError(e, { name });
registerNotification({
id: "workspaces.createError",
text: formatMessage({ id: "workspaces.createError" }),
type: "error",
});
};

return (
<>
{isEditMode ? (
<Card withPadding className={styles.animate}>
<Form<CreateCloudWorkspaceFormValues>
defaultValues={{
name: "",
}}
schema={CreateCloudWorkspaceFormValidationSchema}
onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
>
<FormControl<CreateCloudWorkspaceFormValues>
label={formatMessage({ id: "form.workspaceName" })}
name="name"
fieldType="input"
type="text"
/>
<FormSubmissionButtons
submitKey="form.saveChanges"
onCancelClickCallback={toggleMode}
allowNonDirtyCancel
/>
</Form>
</Card>
) : (
<Box>
<Button
onClick={toggleMode}
variant="secondary"
data-testid="workspaces.createNew"
size="lg"
icon={<Icon type="plus" />}
className={styles.createButton}
>
<FormattedMessage id={isFirstWorkspace ? "workspaces.createFirst" : "workspaces.createNew"} />
</Button>
</Box>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import React from "react";
import { FormattedMessage } from "react-intl";

import { ReactComponent as AirbyteLogo } from "components/illustrations/airbyte-logo.svg";
import { Box } from "components/ui/Box";
import { FlexContainer } from "components/ui/Flex";
import { Heading } from "components/ui/Heading";
import { Text } from "components/ui/Text";

import { useCreateCloudWorkspace, useListCloudWorkspaces } from "core/api/cloud";
import { useTrackPage, PageTrackingCodes } from "core/services/analytics";
import WorkspacesList from "pages/workspaces/components/WorkspacesList";

import { CloudWorkspacesCreateControl } from "./CloudWorkspacesCreateControl";
import styles from "./CloudWorkspacesPage.module.scss";

export const CloudWorkspacesPage: React.FC = () => {
Expand All @@ -26,9 +27,10 @@ export const CloudWorkspacesPage: React.FC = () => {
<Text align="center" className={styles.subtitle}>
<FormattedMessage id="workspaces.subtitle" />
</Text>
<Box pb="2xl">
<WorkspacesList workspaces={workspaces} createWorkspace={createWorkspace} />
</Box>
<FlexContainer direction="column">
<CloudWorkspacesCreateControl createWorkspace={createWorkspace} />
<WorkspacesList workspaces={workspaces} />
</FlexContainer>
</div>
);
};
9 changes: 7 additions & 2 deletions airbyte-webapp/src/pages/workspaces/WorkspacesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { InfoTooltip } from "components/ui/Tooltip";
import { useCreateWorkspace, useListWorkspaces } from "core/api";
import { useTrackPage, PageTrackingCodes } from "core/services/analytics";

import { WorkspacesCreateControl } from "./components/WorkspacesCreateControl";
import WorkspacesList from "./components/WorkspacesList";
import styles from "./WorkspacesPage.module.scss";

const WorkspacesPage: React.FC = () => {
useTrackPage(PageTrackingCodes.WORKSPACES);
const { workspaces } = useListWorkspaces();

const { mutateAsync: createWorkspace } = useCreateWorkspace();

return (
Expand All @@ -43,8 +45,11 @@ const WorkspacesPage: React.FC = () => {
</FlexContainer>
}
/>
<Box py="2xl" className={styles.content}>
<WorkspacesList workspaces={workspaces} createWorkspace={createWorkspace} />
<Box pt="lg">
<FlexContainer direction="column" className={styles.content}>
<WorkspacesCreateControl createWorkspace={createWorkspace} />
<WorkspacesList workspaces={workspaces} />
</FlexContainer>
</Box>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/**
* As written, this workspace create control can ONLY be used in environments
* where the configdb Permissions table is in use.
*
* That is to say -- it should currently only be used in OSS/Enterprise.
*
* May be migrated to Cloud when:
* - Cloud leverages the configdb Permissions table
* - Cloud has a concept of organizations
* - CloudWorkspaceCreate accepts an organizationId
*
*/

import { UseMutateAsyncFunction } from "@tanstack/react-query";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
Expand All @@ -11,38 +24,49 @@ import { Box } from "components/ui/Box";
import { Button } from "components/ui/Button";
import { Card } from "components/ui/Card";
import { Icon } from "components/ui/Icon";
import { Text } from "components/ui/Text";

import { useListWorkspaces } from "core/api";
import { CloudWorkspaceRead } from "core/api/types/CloudApi";
import { WorkspaceRead } from "core/request/AirbyteClient";
import { OrganizationRead, WorkspaceCreate, WorkspaceRead } from "core/request/AirbyteClient";
import { trackError } from "core/utils/datadog";
import { useNotificationService } from "hooks/services/Notification";

import { useOrganizationsToCreateWorkspaces } from "./useOrganizationsToCreateWorkspaces";
import styles from "./WorkspacesCreateControl.module.scss";

interface CreateWorkspaceFormValues {
name: string;
organizationId: string;
}

const CreateWorkspaceFormValidationSchema = yup.object().shape({
const OrganizationCreateWorkspaceFormValidationSchema = yup.object().shape({
name: yup.string().trim().required("form.empty.error"),
organizationId: yup.string().trim().required("form.empty.error"),
});

interface WorkspacesCreateControlProps {
createWorkspace: UseMutateAsyncFunction<WorkspaceRead | CloudWorkspaceRead, unknown, string, unknown>;
interface OrganizationWorkspacesCreateControlProps {
createWorkspace: UseMutateAsyncFunction<WorkspaceRead, unknown, WorkspaceCreate, unknown>;
}

export const WorkspacesCreateControl: React.FC<WorkspacesCreateControlProps> = ({ createWorkspace }) => {
export const WorkspacesCreateControl: React.FC<OrganizationWorkspacesCreateControlProps> = ({ createWorkspace }) => {
const organizations = useOrganizationsToCreateWorkspaces();

const navigate = useNavigate();
const { formatMessage } = useIntl();
const [isEditMode, toggleMode] = useToggle(false);
const { registerNotification } = useNotificationService();
const { workspaces } = useListWorkspaces();
const isFirstWorkspace = workspaces.length === 0;

const onSubmit = async ({ name }: CreateWorkspaceFormValues) => {
const newWorkspace = await createWorkspace(name);
// if the user does not have create permissions in any organizations, do not show the control at all
if (organizations.length === 0) {
return null;
}

const onSubmit = async (values: CreateWorkspaceFormValues) => {
const newWorkspace = await createWorkspace(values);
toggleMode();
newWorkspace && navigate(`/workspaces/${newWorkspace.workspaceId}`);
navigate(`/workspaces/${newWorkspace.workspaceId}`);
};

const onSuccess = () => {
Expand All @@ -69,18 +93,14 @@ export const WorkspacesCreateControl: React.FC<WorkspacesCreateControlProps> = (
<Form<CreateWorkspaceFormValues>
defaultValues={{
name: "",
organizationId: organizations[0].organizationId,
}}
schema={CreateWorkspaceFormValidationSchema}
schema={OrganizationCreateWorkspaceFormValidationSchema}
onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
>
<FormControl<CreateWorkspaceFormValues> label="Workspace name" name="name" fieldType="input" type="text" />
<FormSubmissionButtons
submitKey="form.saveChanges"
onCancelClickCallback={toggleMode}
allowNonDirtyCancel
/>
<WorkspaceCreateControlFormContent organizations={organizations} toggleMode={toggleMode} />
</Form>
</Card>
) : (
Expand All @@ -100,3 +120,47 @@ export const WorkspacesCreateControl: React.FC<WorkspacesCreateControlProps> = (
</>
);
};

const WorkspaceCreateControlFormContent: React.FC<{ organizations: OrganizationRead[]; toggleMode: () => void }> = ({
organizations,
toggleMode,
}) => {
const { formatMessage } = useIntl();

return (
<>
{organizations.length > 1 && (
<FormControl<CreateWorkspaceFormValues>
label={formatMessage({ id: "form.organizationName" })}
name="organizationId"
fieldType="dropdown"
options={organizations.map((organization) => {
return {
value: organization.organizationId,
label: organization.organizationName,
};
})}
/>
)}
<>
<FormControl<CreateWorkspaceFormValues>
label={formatMessage({ id: "form.workspaceName" })}
name="name"
fieldType="input"
type="text"
/>
{organizations.length === 1 && (
<Box pb="md">
<Text>
<FormattedMessage
id="workspaces.create.organizationDescription"
values={{ organizationName: organizations[0].organizationName }}
/>
</Text>
</Box>
)}
<FormSubmissionButtons submitKey="form.saveChanges" onCancelClickCallback={toggleMode} allowNonDirtyCancel />
</>
</>
);
};
Loading

0 comments on commit efe23db

Please sign in to comment.