Skip to content

Commit

Permalink
Use object permissions in object details and object items (#4606)
Browse files Browse the repository at this point in the history
* remove permission from branch selector

* check permission for object details and items

* update types

* update mock data

* fix loading state for items

* update test for role-management (will be improved)

* skip anonymous test for now

* use admin auth for artifacts

* fix artifacts details view with object permission

* fix filter test

* add kind for filter in use object items for permission

* fix auth for object list

* fix auth

* skip anonymous profile test

* skip profiles

* use auth in hierarchical view test

* fix search e2e

* fix token.spec.ts

* fix profile

* permission for profile

* add flag for artifact

* remove log

* fix unauth test

* update permissions structure to not provide a message if it's allowed + relationship example

* add message from api on 403

* update message

* update message

* start update messages and use new logic with current branch

* update types + renaming

* better message

* disable generic selector item when not allowed to create

* moved to permission folder

* enabled skipped tests

* fix mock data

* fix number pool

* fix profiles spec

* fix error causing

* fix tutorial

* cleaning

* cleaning 2

---------

Co-authored-by: bilalabbad <[email protected]>
  • Loading branch information
pa-lem and bilalabbad authored Oct 17, 2024
1 parent 852a6d8 commit cb5f267
Show file tree
Hide file tree
Showing 46 changed files with 621 additions and 149 deletions.
10 changes: 5 additions & 5 deletions frontend/app/src/components/branch-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { QSP } from "@/config/qsp";
import { Branch } from "@/generated/graphql";
import { usePermission } from "@/hooks/usePermission";
import { branchesState, currentBranchAtom } from "@/state/atoms/branches.atom";
import { branchesToSelectOptions } from "@/utils/branches";
import { Icon } from "@iconify-icon/react";
Expand All @@ -12,6 +11,7 @@ import { StringParam, useQueryParam } from "use-query-params";
import { ComboboxItem } from "@/components/ui/combobox";
import { Command, CommandEmpty, CommandInput, CommandList } from "@/components/ui/command";
import graphqlClient from "@/graphql/graphqlClientApollo";
import { useAuth } from "@/hooks/useAuth";
import { constructPath } from "@/utils/fetch";
import { useSetAtom } from "jotai";
import { Button, ButtonWithTooltip, LinkButton } from "./buttons/button-primitive";
Expand Down Expand Up @@ -162,13 +162,13 @@ function BranchOption({ branch, onChange }: { branch: Branch; onChange: () => vo
}

export const BranchFormTriggerButton = ({ setOpen }: { setOpen: (open: boolean) => void }) => {
const permission = usePermission();
const { isAuthenticated } = useAuth();

return (
<ButtonWithTooltip
disabled={!permission.write.allow}
tooltipEnabled={!permission.write.allow}
tooltipContent={permission.write.message ?? undefined}
disabled={!isAuthenticated}
tooltipEnabled={!isAuthenticated}
tooltipContent={"You need to be authenticated."}
className="h-8 w-8 shadow-none"
onKeyDown={(e) => {
if (e.key === "Enter") {
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/components/form/generic-object-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ interface GenericObjectFormProps extends Omit<ObjectFormProps, "kind"> {
}

export const GenericObjectForm = ({ genericSchema, ...props }: GenericObjectFormProps) => {
const [kindToCreate, setKindToCreate] = useState<string | undefined>(
genericSchema.used_by?.length === 1 ? genericSchema.used_by[0] : undefined
const [kindToCreate, setKindToCreate] = useState<string | null>(
genericSchema.used_by?.length === 1 ? genericSchema.used_by[0] : null
);

if (!genericSchema.used_by || genericSchema.used_by?.length === 0) {
Expand Down
75 changes: 68 additions & 7 deletions frontend/app/src/components/form/generic-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
import { Combobox, tComboboxItem } from "@/components/ui/combobox-legacy";
import { Badge } from "@/components/ui/badge";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from "@/components/ui/combobox";
import Label from "@/components/ui/label";
import { PROFILE_KIND } from "@/config/constants";
import useQuery from "@/hooks/useQuery";
import { useSchema } from "@/hooks/useSchema";
import LoadingScreen from "@/screens/loading-screen/loading-screen";
import { getObjectPermissionsQuery } from "@/screens/permission/queries/getObjectPermissions";
import { PermissionData } from "@/screens/permission/types";
import { getPermission } from "@/screens/permission/utils";
import { genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom";
import { gql } from "@apollo/client";
import { useAtomValue } from "jotai/index";
import { useId } from "react";
import React, { useId, useState } from "react";

type GenericSelectorProps = {
currentKind: string;
kindInheritingFromGeneric: string[];
value?: string;
onChange: (item: string) => void;
value?: string | null;
onChange: (item: string | null) => void;
};

export const GenericSelector = ({
currentKind,
kindInheritingFromGeneric,
...props
value,
onChange,
}: GenericSelectorProps) => {
const id = useId();
const nodeSchemas = useAtomValue(schemaState);
const nodeGenerics = useAtomValue(genericsState);
const profileSchemas = useAtomValue(profilesAtom);
const { schema } = useSchema(value);
const [open, setOpen] = useState(false);
const { data, loading } = useQuery(gql(getObjectPermissionsQuery(currentKind)));

const items: Array<tComboboxItem> = kindInheritingFromGeneric
if (loading) return <LoadingScreen />;
const permissionsData: Array<{ node: PermissionData }> = data?.[currentKind]?.permissions?.edges;

const items = kindInheritingFromGeneric
.map((usedByKind) => {
const relatedSchema = [...nodeSchemas, ...profileSchemas].find(
(schema) => schema.kind === usedByKind
Expand Down Expand Up @@ -62,7 +84,46 @@ export const GenericSelector = ({
return (
<div className="p-4 bg-gray-200">
<Label htmlFor={id}>Select an object type</Label>
<Combobox id={id} items={items} {...props} />
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger id={id}>
{schema && <SchemaItem label={schema.label as string} badge={schema.namespace} />}
</ComboboxTrigger>

<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No schema found.</ComboboxEmpty>
{items.map((item) => {
const itemValue = item?.value as string;
const permissionToCreate = getPermission(
permissionsData.filter(({ node }) => node.kind === itemValue)
).create;

return (
<ComboboxItem
key={itemValue}
value={itemValue}
selectedValue={value}
onSelect={() => {
onChange(value === itemValue ? null : itemValue);
setOpen(false);
}}
disabled={!permissionToCreate.isAllowed}
>
<SchemaItem label={item.label} badge={item.badge} />
</ComboboxItem>
);
})}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
);
};

const SchemaItem = ({ label, badge }: { label: string; badge: string }) => {
return (
<div className="flex justify-between w-full">
<span>{label}</span> <Badge>{badge}</Badge>
</div>
);
};
18 changes: 6 additions & 12 deletions frontend/app/src/components/form/object-create-form-trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import SlideOver, { SlideOverTitle } from "@/components/display/slide-over";
import ObjectForm from "@/components/form/object-form";
import { ACCOUNT_GENERIC_OBJECT, ARTIFACT_OBJECT } from "@/config/constants";
import { ARTIFACT_OBJECT } from "@/config/constants";
import graphqlClient from "@/graphql/graphqlClientApollo";
import { usePermission } from "@/hooks/usePermission";
import { Permission } from "@/screens/permission/types";
import { IModelSchema } from "@/state/atoms/schema.atom";
import { isGeneric } from "@/utils/common";
import { Icon } from "@iconify-icon/react";
import { useState } from "react";
import { Button, ButtonProps } from "../buttons/button-primitive";
Expand All @@ -13,32 +12,27 @@ import { Tooltip } from "../ui/tooltip";
interface ObjectCreateFormTriggerProps extends ButtonProps {
schema: IModelSchema;
onSuccess?: (newObject: any) => void;
permission: Permission;
}

export const ObjectCreateFormTrigger = ({
schema,
onSuccess,
isLoading,
permission,
...props
}: ObjectCreateFormTriggerProps) => {
const permission = usePermission();

const [showCreateDrawer, setShowCreateDrawer] = useState(false);

if (schema.kind === ARTIFACT_OBJECT) {
return null;
}

const isAccount: boolean =
schema.kind === ACCOUNT_GENERIC_OBJECT ||
(!isGeneric(schema) && !!schema.inherit_from?.includes(ACCOUNT_GENERIC_OBJECT));

const isAllowed = isAccount ? permission.isAdmin.allow : permission.write.allow;
const tooltipMessage = isAccount ? permission.isAdmin.message : permission.isAdmin.message;
const isAllowed = permission.create.isAllowed;

return (
<>
<Tooltip enabled={!isAllowed} content={tooltipMessage}>
<Tooltip enabled={!isAllowed} content={!isAllowed && permission.create.message}>
<Button
data-cy="create"
data-testid="create-object-button"
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/src/components/time-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const TimeFrameSelector = () => {
selected={date}
onChange={onChange}
showTimeSelect
timeIntervals={10}
timeIntervals={1}
calendarStartDay={1}
maxDate={new Date()}
filterTime={(date) => isPast(date)}
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/src/graphql/graphqlClientApollo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export const errorLink = onError(({ graphQLErrors, operation, forward }) => {
forward(operation);
});
}
case 403: {
// Do not display alert on unauthorized errors
return;
}
default:
const { processErrorMessage } = operation.getContext();

Expand Down
14 changes: 14 additions & 0 deletions frontend/app/src/graphql/queries/objects/getObjectDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ query {{kind}} {
{{/if}}
}
}
{{#if hasPermissions}}
permissions {
edges {
node {
kind
view
create
update
delete
}
}
}
{{/if}}
}
{{#if taskKind}}
Expand Down
14 changes: 14 additions & 0 deletions frontend/app/src/graphql/queries/objects/getObjectItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ query {{kind}} (
{{/each}}
}
}
{{#if hasPermissions}}
permissions {
edges {
node {
kind
view
create
update
delete
}
}
}
{{/if}}
}
}
`);
17 changes: 16 additions & 1 deletion frontend/app/src/hooks/useObjectDetails.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PROFILE_KIND, TASK_OBJECT } from "@/config/constants";
import { getObjectDetailsPaginated } from "@/graphql/queries/objects/getObjectDetails";
import useQuery from "@/hooks/useQuery";
import { getPermission } from "@/screens/permission/utils";
import { IModelSchema, genericsState } from "@/state/atoms/schema.atom";
import { isGeneric } from "@/utils/common";
import { getSchemaObjectColumns, getTabs } from "@/utils/getSchemaObjectColumns";
Expand All @@ -14,6 +15,7 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => {
const relationshipsTabs = getTabs(schema);
const columns = getSchemaObjectColumns({ schema });

const isProfileSchema = schema.namespace === "Profile";
const query = gql(
schema
? getObjectDetailsPaginated({
Expand All @@ -28,14 +30,27 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => {
schema?.kind !== PROFILE_KIND &&
!isGeneric(schema) &&
schema?.generate_profile,
hasPermissions: !isProfileSchema,
})
: // Empty query to make the gql parsing work
// TODO: Find another solution for queries while loading schema
"query { ok }"
);

return useQuery(query, {
const apolloQuery = useQuery(query, {
skip: !schema,
notifyOnNetworkStatusChange: true,
});

const permissionData =
schema?.kind && apolloQuery?.data?.[schema.kind]?.permissions?.edges
? apolloQuery.data[schema.kind].permissions.edges
: null;

const permission = getPermission(permissionData);

return {
...apolloQuery,
permission,
};
};
32 changes: 30 additions & 2 deletions frontend/app/src/hooks/useObjectItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getTokens } from "@/graphql/queries/accounts/getTokens";
import { getObjectItemsPaginated } from "@/graphql/queries/objects/getObjectItems";
import { Filter } from "@/hooks/useFilters";
import useQuery from "@/hooks/useQuery";
import { getPermission } from "@/screens/permission/utils";
import { IModelSchema, genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom";
import { getObjectAttributes, getObjectRelationships } from "@/utils/getSchemaObjectColumns";
import { gql } from "@apollo/client";
Expand Down Expand Up @@ -50,18 +51,45 @@ const getQuery = (schema?: IModelSchema, filters?: Array<Filter>) => {

const relationships = getObjectRelationships({ schema, forListView: true });

const isProfileSchema = schema.namespace === "Profile";

return getObjectItemsPaginated({
kind: kindFilterSchema?.kind || schema.kind,
attributes,
relationships,
filters: filtersString,
hasPermissions: !isProfileSchema,
});
};

export const useObjectItems = (schema?: IModelSchema, filters?: Array<Filter>) => {
export const useObjectItems = (
schema?: IModelSchema,
filters?: Array<Filter>,
kindFilter?: string
) => {
const query = gql`
${getQuery(schema, filters)}
`;

return useQuery(query, { notifyOnNetworkStatusChange: true, skip: !schema });
const apolloQuery = useQuery(query, { notifyOnNetworkStatusChange: true, skip: !schema });

const currentKind = kindFilter || schema?.kind;
const hasPermission = !!(
currentKind &&
apolloQuery?.data &&
apolloQuery?.data[currentKind]?.permissions
);

const permissionData = hasPermission
? apolloQuery.data[currentKind].permissions?.edges
? apolloQuery.data[currentKind].permissions.edges
: apolloQuery.data[currentKind].permissions
: null;

const permission = getPermission(permissionData);

return {
...apolloQuery,
permission,
};
};
2 changes: 1 addition & 1 deletion frontend/app/src/hooks/useSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@/state/atoms/schema.atom";
import { useAtomValue } from "jotai/index";

type UseSchema = (kind?: string) =>
type UseSchema = (kind?: string | null) =>
| {
schema: iNodeSchema;
isGeneric: false;
Expand Down
Loading

0 comments on commit cb5f267

Please sign in to comment.