Skip to content

Commit

Permalink
[dashboard] use organization v2 shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge committed Nov 1, 2023
1 parent 0e8f0de commit b86086e
Show file tree
Hide file tree
Showing 27 changed files with 683 additions and 500 deletions.
4 changes: 3 additions & 1 deletion components/dashboard/src/components/AuthorizeGit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Button } from "./Button";
import { Heading2, Heading3, Subheading } from "./typography/headings";
import classNames from "classnames";
import { iconForAuthProvider, simplifyProviderName } from "../provider-utils";
import { useIsOwner } from "../data/organizations/members-query";

export function useNeedsGitAuthorization() {
const authProviders = useAuthProviders();
Expand All @@ -29,6 +30,7 @@ export function useNeedsGitAuthorization() {
export const AuthorizeGit: FC<{ className?: string }> = ({ className }) => {
const { setUser } = useContext(UserContext);
const org = useCurrentOrg();
const owner = useIsOwner(org.data?.id);
const authProviders = useAuthProviders();
const updateUser = useCallback(() => {
getGitpodService().server.getLoggedInUser().then(setUser);
Expand Down Expand Up @@ -57,7 +59,7 @@ export const AuthorizeGit: FC<{ className?: string }> = ({ className }) => {
{verifiedProviders.length === 0 ? (
<>
<Heading3 className="pb-2">No Git integrations</Heading3>
{!!org.data?.isOwner ? (
{!!owner ? (
<div className="px-6">
<Subheading>You need to configure at least one Git integration.</Subheading>
<Link to="/settings/git">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Button } from "./Button";
import { useCreateHoldPaymentIntentMutation } from "../data/billing/create-hold-payment-intent-mutation";
import { useToast } from "./toasts/Toasts";
import { ProgressBar } from "./ProgressBar";
import { useListOrganizationMembers } from "../data/organizations/members-query";

const BASE_USAGE_LIMIT_FOR_STRIPE_USERS = 1000;

Expand All @@ -33,8 +34,9 @@ let didAlreadyCallSubscribe = false;

export default function UsageBasedBillingConfig({ hideSubheading = false }: Props) {
const currentOrg = useCurrentOrg().data;
const attrId = currentOrg ? AttributionId.create(currentOrg) : undefined;
const attrId = currentOrg ? AttributionId.createFromOrganizationId(currentOrg.id) : undefined;
const attributionId = attrId && AttributionId.render(attrId);
const members = useListOrganizationMembers(currentOrg?.id).data;
const [showUpdateLimitModal, setShowUpdateLimitModal] = useState<boolean>(false);
const [stripeSubscriptionId, setStripeSubscriptionId] = useState<string | undefined>();
const [isLoadingStripeSubscription, setIsLoadingStripeSubscription] = useState<boolean>(true);
Expand Down Expand Up @@ -155,7 +157,7 @@ export default function UsageBasedBillingConfig({ hideSubheading = false }: Prop
// FIXME: Should we ask the customer to confirm or edit this default limit?
let limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS;
if (attrId?.kind === "team" && currentOrg) {
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * currentOrg.members.length;
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * (members?.length || 0);
}
const newLimit = await getGitpodService().server.subscribeToStripe(
attributionId,
Expand Down Expand Up @@ -190,7 +192,7 @@ export default function UsageBasedBillingConfig({ hideSubheading = false }: Prop
);
}
},
[attrId?.kind, attributionId, currentOrg, location.pathname, refreshSubscriptionDetails],
[members, attrId?.kind, attributionId, currentOrg, location.pathname, refreshSubscriptionDetails],
);

const showSpinner = !attributionId || isLoadingStripeSubscription || isCreatingSubscription;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* See License.AGPL.txt in the project root for license information.
*/

import { Organization } from "@gitpod/gitpod-protocol";
import { useMutation } from "@tanstack/react-query";
import { useOrganizationsInvalidator } from "./orgs-query";
import { publicApiTeamToProtocol, teamsService } from "../../service/public-api";
import { organizationClient } from "../../service/public-api";
import { Organization } from "@gitpod/public-api/lib/gitpod/experimental/v2/organization_pb";

type CreateOrgArgs = Pick<Organization, "name">;

Expand All @@ -16,13 +16,12 @@ export const useCreateOrgMutation = () => {

return useMutation<Organization, Error, CreateOrgArgs>({
mutationFn: async ({ name }) => {
const { team } = await teamsService.createTeam({ name });
if (!team) {
throw new Error("Error creating team");
const { organization } = await organizationClient.createOrganization({ name });
if (!organization) {
throw new Error("Error creating organization");
}

const org = publicApiTeamToProtocol(team);
return org;
return organization;
},
onSuccess(newOrg) {
invalidateOrgs();
Expand Down
54 changes: 54 additions & 0 deletions components/dashboard/src/data/organizations/invite-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { organizationClient } from "../../service/public-api";

export function useInviteInvalidator(organizationId: string | undefined) {
const queryClient = useQueryClient();
return useCallback(() => {
queryClient.invalidateQueries(getQueryKey(organizationId));
}, [organizationId, queryClient]);
}

export function useInvitationId(organizationId: string | undefined) {
const query = useQuery<string, Error>(
getQueryKey(organizationId),
async () => {
const response = await organizationClient.getOrganizationInvitation({
organizationId,
});
return response.invitationId;
},
{
enabled: !!organizationId,
},
);
return query;
}

export function useResetInvitationId(organizationId: string | undefined) {
const invalidate = useInviteInvalidator(organizationId);
return useMutation<void, Error, string>({
mutationFn: async (orgId) => {
if (!orgId) {
throw new Error("No current organization selected");
}

await organizationClient.resetOrganizationInvitation({
organizationId: orgId,
});
},
onSuccess(updatedOrg) {
invalidate();
},
});
}

function getQueryKey(organizationId: string | undefined) {
return ["invitationId", organizationId || "undefined"];
}
50 changes: 50 additions & 0 deletions components/dashboard/src/data/organizations/members-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { OrganizationMember, OrganizationRole } from "@gitpod/public-api/lib/gitpod/experimental/v2/organization_pb";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { organizationClient } from "../../service/public-api";
import { useCurrentUser } from "../../user-context";

export function useOrganizationMembersInvalidator(organizationId: string | undefined) {
const queryClient = useQueryClient();
return useCallback(() => {
queryClient.invalidateQueries(getQueryKey(organizationId));
}, [organizationId, queryClient]);
}

export function useListOrganizationMembers(organizationId: string | undefined) {
const query = useQuery<OrganizationMember[], Error>(
getQueryKey(organizationId),
async () => {
const response = await organizationClient.listOrganizationMembers({
organizationId,
pagination: {
pageSize: 1000,
},
});
return response.members;
},
{
enabled: !!organizationId,
},
);
return query;
}

export function useIsOwner(organizationId: string | undefined): boolean {
const user = useCurrentUser();
const members = useListOrganizationMembers(organizationId);
const isOwner = useMemo(() => {
return members?.data?.some((m) => m.userId === user?.id && m.role === OrganizationRole.OWNER);
}, [members?.data, user?.id]);
return !!isOwner;
}

function getQueryKey(organizationId: string | undefined) {
return ["listOrganizationMembers", organizationId || "undefined"];
}
44 changes: 25 additions & 19 deletions components/dashboard/src/data/organizations/org-settings-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@
* See License.AGPL.txt in the project root for license information.
*/

import { OrganizationSettings } from "@gitpod/gitpod-protocol";
import { useQuery } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { useCurrentOrg } from "./orgs-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { organizationClient } from "../../service/public-api";
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/experimental/v2/organization_pb";
import { useCallback } from "react";

export type OrgSettingsResult = OrganizationSettings;
export function useOrgSettingsQueryInvalidator(organizationId?: string) {
const queryClient = useQueryClient();
return useCallback(() => {
queryClient.invalidateQueries(getQueryKey(organizationId));
}, [organizationId, queryClient]);
}

export const useOrgSettingsQuery = () => {
const org = useCurrentOrg().data;

return useQuery<OrgSettingsResult>({
queryKey: getOrgSettingsQueryKey(org?.id ?? ""),
staleTime: 1000 * 60 * 1, // 1 minute
queryFn: async () => {
if (!org) {
export function useOrgSettingsQuery(organizationId?: string) {
return useQuery<OrganizationSettings, Error>(
getQueryKey(organizationId),
async () => {
if (!organizationId) {
throw new Error("No org selected.");
}

const settings = await getGitpodService().server.getOrgSettings(org.id);
return settings || null;
const settings = await organizationClient.getOrganizationSettings({ organizationId });
return settings.settings || new OrganizationSettings();
},
{
enabled: !!organizationId,
},
enabled: !!org,
});
};
);
}

export const getOrgSettingsQueryKey = (teamId: string) => ["org-settings", { teamId }];
function getQueryKey(organizationId?: string) {
return ["getOrganizationSettings", organizationId || "undefined"];
}
30 changes: 7 additions & 23 deletions components/dashboard/src/data/organizations/orgs-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@
* See License.AGPL.txt in the project root for license information.
*/

import { Organization, OrgMemberInfo, User } from "@gitpod/gitpod-protocol";
import { User } from "@gitpod/gitpod-protocol";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { useLocation } from "react-router";
import { publicApiTeamMembersToProtocol, publicApiTeamToProtocol, teamsService } from "../../service/public-api";
import { organizationClient } from "../../service/public-api";
import { useCurrentUser } from "../../user-context";
import { noPersistence } from "../setup";

export interface OrganizationInfo extends Organization {
members: OrgMemberInfo[];
isOwner: boolean;
invitationId?: string;
}
import { Organization } from "@gitpod/public-api/lib/gitpod/experimental/v2/organization_pb";

export function useOrganizationsInvalidator() {
const user = useCurrentUser();
Expand All @@ -29,7 +24,7 @@ export function useOrganizationsInvalidator() {

export function useOrganizations() {
const user = useCurrentUser();
const query = useQuery<OrganizationInfo[], Error>(
const query = useQuery<Organization[], Error>(
getQueryKey(user),
async () => {
console.log("Fetching orgs... " + JSON.stringify(getQueryKey(user)));
Expand All @@ -38,19 +33,8 @@ export function useOrganizations() {
return [];
}

const response = await teamsService.listTeams({});
const result: OrganizationInfo[] = [];
for (const org of response.teams) {
const members = publicApiTeamMembersToProtocol(org.members || []);
const isOwner = members.some((m) => m.role === "owner" && m.userId === user?.id);
result.push({
...publicApiTeamToProtocol(org),
members,
isOwner,
invitationId: org.teamInvitation?.id,
});
}
return result;
const response = await organizationClient.listOrganizations({});
return response.organizations;
},
{
enabled: !!user,
Expand All @@ -68,7 +52,7 @@ function getQueryKey(user?: User) {
}

// Custom hook to return the current org if one is selected
export function useCurrentOrg(): { data?: OrganizationInfo; isLoading: boolean } {
export function useCurrentOrg(): { data?: Organization; isLoading: boolean } {
const location = useLocation();
const orgs = useOrganizations();
const user = useCurrentUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
*/

import { OrganizationSettings } from "@gitpod/gitpod-protocol";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { getOrgSettingsQueryKey, OrgSettingsResult } from "./org-settings-query";
import { useOrgSettingsQueryInvalidator } from "./org-settings-query";
import { useCurrentOrg } from "./orgs-query";

type UpdateOrganizationSettingsArgs = Partial<
Pick<OrganizationSettings, "workspaceSharingDisabled" | "defaultWorkspaceImage">
>;

export const useUpdateOrgSettingsMutation = () => {
const queryClient = useQueryClient();
const team = useCurrentOrg().data;
const teamId = team?.id || "";
const org = useCurrentOrg().data;
const invalidator = useOrgSettingsQueryInvalidator(org?.id);
const teamId = org?.id || "";

return useMutation<OrganizationSettings, Error, UpdateOrganizationSettingsArgs>({
mutationFn: async ({ workspaceSharingDisabled, defaultWorkspaceImage }) => {
Expand All @@ -26,10 +26,6 @@ export const useUpdateOrgSettingsMutation = () => {
defaultWorkspaceImage,
});
},
onSuccess: (newData, _) => {
const queryKey = getOrgSettingsQueryKey(teamId);
queryClient.setQueryData<OrgSettingsResult>(queryKey, newData);
queryClient.invalidateQueries({ queryKey });
},
onSuccess: invalidator,
});
};
29 changes: 29 additions & 0 deletions components/dashboard/src/data/setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { Organization } from "@gitpod/public-api/lib/gitpod/experimental/v2/organization_pb";
import { Timestamp } from "@bufbuild/protobuf";
import { hydrate, dehydrate } from "./setup";

test("set and get proto message", async () => {
const now = new Date();
const org = new Organization({
creationTime: Timestamp.fromDate(now),
id: "test-id",
name: "test-name",
slug: "test-slug",
});

expect(rehydrate(org).creationTime!.toDate()).toStrictEqual(now);
});

function rehydrate<T>(obj: T): T {
const dehydrated = dehydrate(obj);
const str = JSON.stringify(dehydrated);
const fromStorage = JSON.parse(str);
const hydrated = hydrate(fromStorage);
return hydrated;
}
Loading

0 comments on commit b86086e

Please sign in to comment.