Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New GUI - add project info panel #3846

Open
wants to merge 1 commit into
base: cloud-pipeline-new-ui
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions portals-ui/packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export * from './projects';
export * from './metadata';
export * from './runs';
export * from './data-storages';
export * from './permissions';

export { cloudPipelineApi };
26 changes: 26 additions & 0 deletions portals-ui/packages/api/src/permissions/fetch-permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AclClass } from '@cloud-pipeline/core';
import cloudPipelineApi from '../cloud-pipeline-api/index.ts';

export type PermissionsResponse = {
entityId: number;
entityClass: AclClass;
owner: string;
permissions: {
sid: {
name: string;
principal: boolean;
};
mask: number;
}[];
};

export async function fetchPermissions(
id: number,
aclClass: AclClass,
): Promise<PermissionsResponse> {
const query = new URLSearchParams({ id: `${id}`, aclClass }).toString();

return await cloudPipelineApi.jsonGet<PermissionsResponse>({
uri: `permissions?${query}`,
});
}
1 change: 1 addition & 0 deletions portals-ui/packages/api/src/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fetch-permissions';
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import React, { useMemo } from 'react';
import { Converter } from 'showdown';
import classNames from 'classnames';
import './styles.css';
import {
import type {
MarkdownProps,
MarkdownTagRenderer,
MarkdownTagRendererPropsMapper,
} from './types.ts';
import parse from 'html-react-parser';
import { CommonProps } from '../common.types.ts';
import type { CommonProps } from '../common.types.ts';

const converter: Converter = new Converter({
omitExtraWLInCodeBlocks: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { CommonProps } from '@cloud-pipeline/components';
import { Popover } from 'antd';
import { getUserDisplayName } from '@cloud-pipeline/core';
import type { User, UserInfo } from '@cloud-pipeline/core';
import { TooltipPlacement } from 'antd/es/tooltip';
import type { TooltipPlacement } from 'antd/es/tooltip';
import { UserIcon } from '@heroicons/react/24/solid';

export type UserCardProps = CommonProps & {
Expand All @@ -27,6 +27,7 @@ export const UserCard = (props: UserCardProps) => {
className,
style,
} = props;

const userName = useMemo(() => {
if ('name' in user && typeof user.name === 'string') {
return user.name;
Expand All @@ -36,6 +37,7 @@ export const UserCard = (props: UserCardProps) => {
}
return getUserDisplayName(user);
}, [user]);

const renderContent = useCallback(
(user: User | UserInfo) => {
if (user.attributes) {
Expand All @@ -60,17 +62,21 @@ export const UserCard = (props: UserCardProps) => {
},
[userName],
);

if (!user) {
return null;
}

const icon = showIcon ? (
<UserIcon className={classNames('w-3 h-3 mr-0.5', iconClassName)} />
) : null;

const userComponent = (
<span className={className} style={style}>
{user ? getUserDisplayName(user) : userName}
</span>
);

if (!showTooltip || !user) {
return (
<div className="inline-flex whitespace-nowrap items-center">
Expand All @@ -79,6 +85,7 @@ export const UserCard = (props: UserCardProps) => {
</div>
);
}

return (
<span className="inline-flex whitespace-nowrap items-center">
{icon}
Expand Down
53 changes: 53 additions & 0 deletions portals-ui/packages/core/src/utilities/acl-extended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function checkPermission(mask: number, permission: number): boolean {
return (mask & permission) == permission;
}
const READ_ALLOWED = 0b000001;
const READ_DENIED = 0b000010;
const WRITE_ALLOWED = 0b000100;
const WRITE_DENIED = 0b001000;
const EXECUTE_ALLOWED = 0b010000;
const EXECUTE_DENIED = 0b100000;

export function readAllowedExtended(mask: number): boolean {
return checkPermission(mask, READ_ALLOWED);
}

export function readInheritedExtended(mask: number): boolean {
return (
!checkPermission(mask, READ_ALLOWED) && !checkPermission(mask, READ_DENIED)
);
}

export function readDeniedExtended(mask: number): boolean {
return checkPermission(mask, READ_DENIED);
}

export function writeAllowedExtended(mask: number): boolean {
return checkPermission(mask, WRITE_ALLOWED);
}

export function writeInheritedExtended(mask: number): boolean {
return (
!checkPermission(mask, WRITE_ALLOWED) &&
!checkPermission(mask, WRITE_DENIED)
);
}

export function writeDeniedExtended(mask: number): boolean {
return checkPermission(mask, WRITE_DENIED);
}

export function executeAllowedExtended(mask: number): boolean {
return checkPermission(mask, EXECUTE_ALLOWED);
}

export function executeInheritedExtended(mask: number): boolean {
return (
!checkPermission(mask, EXECUTE_ALLOWED) &&
!checkPermission(mask, EXECUTE_DENIED)
);
}

export function executeDeniedExtended(mask: number): boolean {
return checkPermission(mask, EXECUTE_DENIED);
}
1 change: 1 addition & 0 deletions portals-ui/packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './acl';
export * from './acl-extended';
export * from './misc';
export * from './users';
export * from './dates';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { ProjectHeader } from './project-header';
export { ProjectPipelines } from './project-pipelines';
export { ProjectPermissions } from './project-permissions';
export { ProjectRunsList } from './project-runs-list';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProjectPermissions } from './project-permissions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { PermissionsResponse } from '@cloud-pipeline/api';
import type { CommonProps } from '@cloud-pipeline/components';
import { UserCard } from '@cloud-pipeline/components';
import type { UserInfo } from '@cloud-pipeline/core';
import {
readAllowedExtended,
writeAllowedExtended,
executeAllowedExtended,
readInheritedExtended,
writeInheritedExtended,
executeInheritedExtended,
} from '@cloud-pipeline/core';
import { UserIcon, UsersIcon } from '@heroicons/react/24/solid';
import type { TagProps } from 'antd';
import { Tag } from 'antd';
import cn from 'classnames';

type PermissionTagProps = {
isInherited: boolean;
isAllowed: boolean;
label: string;
color: TagProps['color'];
};

const PermissionTag = ({
isInherited,
isAllowed,
label,
color,
}: PermissionTagProps) => {
return (
<Tag
color={isAllowed ? color : 'default'}
className={cn({
'border-none text-gray-400': isInherited,
})}>
{label}
</Tag>
);
};

type PermissionRowProps = CommonProps & {
permission: PermissionsResponse['permissions'][0];
usersInfo: UserInfo[];
};

export const PermissionRow = ({
permission,
usersInfo,
className,
style,
}: PermissionRowProps) => {
const { mask, sid } = permission;
const isUser = sid.principal;

const currentUser = isUser
? usersInfo?.find((user) => user.name === sid.name)
: null;

return (
<div
className={cn('px-3 py-2 border-b min-w-[220px]', className)}
style={style}>
<p className="flex items-center gap-x-0.5">
{isUser ? (
<UserIcon className="w-3 h-3 mr-0.5" />
) : (
<UsersIcon className="w-3 h-3 mr-0.5" />
)}

{currentUser ? <UserCard user={currentUser} /> : <p>{sid.name}</p>}
</p>

<div className="flex gap-x-0.5 mt-2 ml-4">
<PermissionTag
isAllowed={readAllowedExtended(mask)}
isInherited={readInheritedExtended(mask)}
label="Read"
color="blue"
/>
<PermissionTag
isAllowed={writeAllowedExtended(mask)}
isInherited={writeInheritedExtended(mask)}
label="Write"
color="orange"
/>
<PermissionTag
isAllowed={executeAllowedExtended(mask)}
isInherited={executeInheritedExtended(mask)}
label="Execute"
color="green"
/>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AclClass } from '@cloud-pipeline/core';
import { usePermissions } from '../../hooks';
import { useUsersInfoState } from '../../../../state/users-info/hooks';
import { PermissionRow } from './permission-row';
import { PageSpinner } from '../../../../shared/ui';

type Props = {
projectId?: number;
};

export const ProjectPermissions = ({ projectId }: Props) => {
const { permissions, isLoading: isPermissionsLoading } = usePermissions(
AclClass.folder,
projectId,
);
const { usersInfo } = useUsersInfoState();

if (isPermissionsLoading) {
return <PageSpinner />;
}

if (!projectId || !usersInfo) {
return <div>No data</div>;
}

if (!permissions?.permissions) {
return <div>No permissions given</div>;
}

return (
<div className="flex flex-wrap gap-x-4">
{permissions?.permissions?.map((permission) => {
return (
<PermissionRow
permission={permission}
usersInfo={usersInfo}
className="flex-grow"
/>
);
})}
</div>
);
};
15 changes: 0 additions & 15 deletions portals-ui/sites/ngs-portal/src/pages/project/constants.ts

This file was deleted.

2 changes: 2 additions & 0 deletions portals-ui/sites/ngs-portal/src/pages/project/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { usePermissions } from './use-permissions';
export { useProjectTabs } from './use-project-tabs';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { PermissionsResponse } from '@cloud-pipeline/api';
import { fetchPermissions } from '@cloud-pipeline/api';
import type { AclClass } from '@cloud-pipeline/core';
import { useState, useEffect } from 'react';

export const usePermissions = (aclClass: AclClass, id?: number) => {
const [permissions, setPermissions] = useState<
PermissionsResponse | undefined
>();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
if (id) {
const getPermissions = async () => {
try {
const response = await fetchPermissions(id, aclClass);
setPermissions(response);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Permissions fetch error');
}
} finally {
setIsLoading(false);
}
};

void getPermissions();
}
}, [aclClass, id]);

return { permissions, error, isLoading };
};
Loading