Implement PermissionsProvider and use in assets page
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -1,53 +1,54 @@
|
||||
import { Link, Navigate, Outlet, useParams } from "react-router";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownSeparator,
|
||||
IconArrowBoxLeft,
|
||||
IconBank,
|
||||
IconBook,
|
||||
IconBox,
|
||||
IconCalendar1,
|
||||
IconCheckmark1,
|
||||
IconChevronGrabberVertical,
|
||||
IconCircleProgress,
|
||||
IconCircleQuestionmark,
|
||||
IconClock,
|
||||
IconCrossLargeX,
|
||||
IconFire3,
|
||||
IconGroup1,
|
||||
IconInboxEmpty,
|
||||
IconKey,
|
||||
IconListStack,
|
||||
IconLock,
|
||||
IconMedal,
|
||||
IconPageTextLine,
|
||||
IconPeopleAdd,
|
||||
IconPlusLarge,
|
||||
IconRotateCw,
|
||||
IconSettingsGear2,
|
||||
IconShield,
|
||||
IconStore,
|
||||
IconTodo,
|
||||
IconListStack,
|
||||
IconBox,
|
||||
IconShield,
|
||||
IconRotateCw,
|
||||
IconCircleProgress,
|
||||
IconMedal,
|
||||
IconCalendar1,
|
||||
Layout,
|
||||
SidebarItem,
|
||||
UserDropdown as UserDropdownRoot,
|
||||
UserDropdownItem,
|
||||
Skeleton,
|
||||
Dropdown,
|
||||
Button,
|
||||
DropdownItem,
|
||||
IconChevronGrabberVertical,
|
||||
IconPlusLarge,
|
||||
Avatar,
|
||||
IconPeopleAdd,
|
||||
Badge,
|
||||
IconKey,
|
||||
IconLock,
|
||||
UserDropdownItem,
|
||||
UserDropdown as UserDropdownRoot,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
|
||||
import { Suspense, useState, useEffect } from "react";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import { Link, Navigate, Outlet, useParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
|
||||
import { PageError } from "/components/PageError";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
import { Authorized } from "/permissions";
|
||||
import { PermissionsProvider } from "/providers/PermissionsProvider";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
|
||||
const MainLayoutQuery = graphql`
|
||||
query MainLayoutQuery($organizationId: ID!) {
|
||||
@@ -82,7 +83,9 @@ export function MainLayout() {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton className="w-full h-screen" />}>
|
||||
<MainLayoutContent organizationId={organizationId} prefix={prefix} />
|
||||
<PermissionsProvider>
|
||||
<MainLayoutContent organizationId={organizationId} prefix={prefix} />
|
||||
</PermissionsProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
IconPlusLarge,
|
||||
PageHeader,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { use } from "react";
|
||||
import {
|
||||
graphql,
|
||||
type PreloadedQuery,
|
||||
usePaginationFragment,
|
||||
usePreloadedQuery,
|
||||
type PreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { useParams } from "react-router";
|
||||
import { AssetsTable } from "../../../components/assets/AssetsTable";
|
||||
import { ReadOnlyAssetsTable } from "../../../components/assets/ReadOnlyAssetsTable";
|
||||
import type { AssetsPageFragment$key } from "./__generated__/AssetsPageFragment.graphql";
|
||||
import { CreateAssetDialog } from "./dialogs/CreateAssetDialog";
|
||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||
import {
|
||||
assetsQuery,
|
||||
} from "/hooks/graph/AssetGraph";
|
||||
import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql";
|
||||
import type { AssetsPageFragment$key } from "./__generated__/AssetsPageFragment.graphql";
|
||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||
import { CreateAssetDialog } from "./dialogs/CreateAssetDialog";
|
||||
import { Authorized, isAuthorized } from "/permissions";
|
||||
import { AssetsTable } from "../../../components/assets/AssetsTable";
|
||||
import { ReadOnlyAssetsTable } from "../../../components/assets/ReadOnlyAssetsTable";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { PermissionsContext } from "/providers/PermissionsProvider";
|
||||
|
||||
const paginatedAssetsFragment = graphql`
|
||||
fragment AssetsPageFragment on Organization
|
||||
@@ -90,9 +91,10 @@ export default function AssetsPage(props: Props) {
|
||||
const assets = pagination.data.assets?.edges.map((edge) => edge.node);
|
||||
const connectionId = pagination.data.assets.__id;
|
||||
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const canWrite = (
|
||||
isAuthorized(organizationId, "Asset", "updateAsset") ||
|
||||
isAuthorized(organizationId, "Asset", "deleteAsset")
|
||||
isAuthorized("Asset", "updateAsset") ||
|
||||
isAuthorized("Asset", "deleteAsset")
|
||||
);
|
||||
usePageTitle(__("Assets"));
|
||||
|
||||
@@ -105,15 +107,13 @@ export default function AssetsPage(props: Props) {
|
||||
"Manage your organization's assets and their classifications.",
|
||||
)}
|
||||
>
|
||||
{!isSnapshotMode && (
|
||||
<Authorized entity="Organization" action="createAsset">
|
||||
<CreateAssetDialog
|
||||
connection={connectionId}
|
||||
organizationId={organizationId}
|
||||
>
|
||||
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
||||
</CreateAssetDialog>
|
||||
</Authorized>
|
||||
{!isSnapshotMode && isAuthorized("Organization", "createAsset") && (
|
||||
<CreateAssetDialog
|
||||
connection={connectionId}
|
||||
organizationId={organizationId}
|
||||
>
|
||||
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
||||
</CreateAssetDialog>
|
||||
)}
|
||||
</PageHeader>
|
||||
{isSnapshotMode || !canWrite ?
|
||||
|
||||
51
apps/console/src/providers/PermissionsProvider.tsx
Normal file
51
apps/console/src/providers/PermissionsProvider.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createContext, type PropsWithChildren } from "react";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
|
||||
enum Role {
|
||||
OWNER = "OWNER",
|
||||
ADMIN = "ADMIN",
|
||||
VIEWER = "VIEWER",
|
||||
}
|
||||
|
||||
type PermissionsResponse = {
|
||||
permissions: Record<string, Record<string, boolean>>;
|
||||
role: Role;
|
||||
};
|
||||
|
||||
type PermissionsContextType = {
|
||||
isAuthorized: (entity: string, action: string) => boolean;
|
||||
} & PermissionsResponse;
|
||||
|
||||
export const PermissionsContext = createContext<PermissionsContextType>({
|
||||
permissions: {},
|
||||
role: Role.VIEWER,
|
||||
isAuthorized: () => false,
|
||||
});
|
||||
|
||||
export function PermissionsProvider(props: PropsWithChildren) {
|
||||
const { children } = props;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
const { data } = useSuspenseQuery<PermissionsResponse>({
|
||||
queryKey: ["permissions", organizationId],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/authz/${organizationId}/permissions`, { credentials: "include" });
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch permissions");
|
||||
}
|
||||
return response.json() as Promise<PermissionsResponse>;
|
||||
},
|
||||
});
|
||||
|
||||
const isAuthorized = (entity: string, action: string) => {
|
||||
return data.permissions[entity]?.[action] ?? false;
|
||||
}
|
||||
|
||||
return (
|
||||
<PermissionsContext value={{ ...data, isAuthorized }}>
|
||||
{children}
|
||||
</PermissionsContext>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user