Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (3)
Showing
with 709 additions and 38 deletions
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; import {
useAppDispatch,
useSchemaGraph,
useSessionCache,
useAuthorizationCache,
useCheckPermissionPolicy,
} from '@graphpolaris/shared/lib/data-access';
import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
import { SettingsForm } from './forms/settings'; import { SettingsForm } from './forms/settings';
import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner';
...@@ -56,6 +62,29 @@ export default function DatabaseSelector({}) { ...@@ -56,6 +62,29 @@ export default function DatabaseSelector({}) {
}; };
}, [connecting]); }, [connecting]);
const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'database';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
return ( return (
<div className="menu-walkthrough"> <div className="menu-walkthrough">
<TooltipProvider delayDuration={1000}> <TooltipProvider delayDuration={1000}>
...@@ -85,11 +114,14 @@ export default function DatabaseSelector({}) { ...@@ -85,11 +114,14 @@ export default function DatabaseSelector({}) {
> >
<DropdownTrigger <DropdownTrigger
onClick={() => { onClick={() => {
setDbSelectionMenuOpen(!dbSelectionMenuOpen); if (connecting || authCache.authorized === false || !!authCache.roomID || writeAllowed) {
console.debug('User blocked from editing query due to being a viewer');
setDbSelectionMenuOpen(!dbSelectionMenuOpen);
}
}} }}
className="w-[18rem]" className="w-[18rem]"
size="md" size="md"
disabled={connecting || authCache.authorized === false || !!authCache.roomID} disabled={connecting || authCache.authorized === false || !!authCache.roomID || !writeAllowed}
title={ title={
<div className="flex items-center"> <div className="flex items-center">
{connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? ( {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? (
......
...@@ -8,18 +8,24 @@ ...@@ -8,18 +8,24 @@
/* The comment above was added so the code coverage wouldn't count this file towards code coverage. /* The comment above was added so the code coverage wouldn't count this file towards code coverage.
* We do not test components/renderfunctions/styling files. * We do not test components/renderfunctions/styling files.
* See testing plan for more details.*/ * See testing plan for more details.*/
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access'; import { useAuthorizationCache, useAuth, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
import DatabaseSelector from './DatabaseManagement/dbConnectionSelector'; import DatabaseSelector from './DatabaseManagement/dbConnectionSelector';
import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns'; import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
import GpLogo from './gp-logo'; import GpLogo from './gp-logo';
import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover'; import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover';
import { useDispatch } from 'react-redux';
import { Dialog, DialogContent, DialogTrigger } from '@graphpolaris/shared/lib/components/layout/Dialog';
import { UserManagementContent } from '@graphpolaris/shared/lib/components/userManagementContent/UserManagementContent';
import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
export const Navbar = () => { export const Navbar = () => {
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const auth = useAuth(); const auth = useAuth();
const authCache = useAuthorizationCache(); const authCache = useAuthorizationCache();
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const dispatch = useDispatch();
const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
...@@ -33,8 +39,37 @@ export const Navbar = () => { ...@@ -33,8 +39,37 @@ export const Navbar = () => {
}; };
}, [menuOpen]); }, [menuOpen]);
const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'policy';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
const handleConfirmUsers = (users: { name: string; email: string; type: string }[]) => {
//TODO !FIXME: when the user clicks on confirm, users state is ready to be sent to backend
};
const handleClickShareLink = () => {
//TODO !FIXME: add copy link to clipoard functionality
dispatch(addInfo('Link copied to clipboard'));
};
return ( return (
<nav className="w-full px-4 h-12 flex flex-row items-center gap-2 md:gap-3 lg:gap-4"> <nav className="w-full px-4 h-12 flex flex-row items-center gap-2 md:gap-3 lg:gap-4">
<a href="https://graphpolaris.com/" target="_blank" className="shrink-0 text-dark"> <a href="https://graphpolaris.com/" target="_blank" className="shrink-0 text-dark">
...@@ -53,12 +88,12 @@ export const Navbar = () => { ...@@ -53,12 +88,12 @@ export const Navbar = () => {
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-56 z-30 bg-white rounded-sm border-[1px] outline-none"> <PopoverContent className="w-56 z-30 bg-light rounded-sm border-[1px] outline-none">
<div className="p-2 text-sm border-b"> <div className="p-2 text-sm border-b">
<h2 className="font-bold">user: {authCache.username}</h2> <h2 className="font-bold">user: {authCache.username}</h2>
<h3 className="text-xs break-words">session: {authCache.sessionID}</h3> <h3 className="text-xs break-words">session: {authCache.sessionID}</h3>
<h3 className="text-xs break-words">license: Creator</h3>
</div> </div>
{authCache.authorized ? ( {authCache.authorized ? (
<> <>
<DropdownItem <DropdownItem
...@@ -75,7 +110,6 @@ export const Navbar = () => { ...@@ -75,7 +110,6 @@ export const Navbar = () => {
<DropdownItem value="Login" onClick={() => {}} /> <DropdownItem value="Login" onClick={() => {}} />
</> </>
)} )}
{authCache?.roomID && ( {authCache?.roomID && (
<div className="p-2 border-b"> <div className="p-2 border-b">
<h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3> <h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3>
...@@ -84,6 +118,20 @@ export const Navbar = () => { ...@@ -84,6 +118,20 @@ export const Navbar = () => {
<div className="p-2 border-t"> <div className="p-2 border-t">
<h3 className="text-xs">Version: {buildInfo}</h3> <h3 className="text-xs">Version: {buildInfo}</h3>
</div> </div>
{writeAllowed && (
<>
<Dialog>
<DialogTrigger className="ml-2 text-sm hover:bg-secondary-200">Manage Viewers Permission</DialogTrigger>
<DialogContent>
<UserManagementContent
sessionId={authCache.sessionID ?? ''}
onConfirm={handleConfirmUsers}
onClickShareLink={handleClickShareLink}
/>
</DialogContent>
</Dialog>
</>
)}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
......
...@@ -9,7 +9,7 @@ export type VisualizationTooltipProps = { ...@@ -9,7 +9,7 @@ export type VisualizationTooltipProps = {
export const VisualizationTooltip: React.FC<VisualizationTooltipProps> = ({ name, colorHeader, children }) => { export const VisualizationTooltip: React.FC<VisualizationTooltipProps> = ({ name, colorHeader, children }) => {
return ( return (
<div className="border-1 border-sec-200 bg-white w-[12rem] -mx-2 -my-2"> <div className="border-1 border-sec-200 bg-light w-[12rem] -mx-2 -my-2">
<div className="flex m-0 justify-start items-stretch border-b border-sec-200 relative"> <div className="flex m-0 justify-start items-stretch border-b border-sec-200 relative">
<div className="left-0 top-0 h-auto w-1.5" style={{ backgroundColor: colorHeader }}></div> <div className="left-0 top-0 h-auto w-1.5" style={{ backgroundColor: colorHeader }}></div>
<div className="px-2.5 py-1 truncate flex"> <div className="px-2.5 py-1 truncate flex">
......
...@@ -78,6 +78,11 @@ type BooleanProps = { ...@@ -78,6 +78,11 @@ type BooleanProps = {
value: boolean; value: boolean;
tooltip?: string; tooltip?: string;
onChange?: (value: boolean) => void; onChange?: (value: boolean) => void;
size?: 'xs' | 'sm' | 'md' | 'xl';
info?: string;
required?: boolean;
className?: string;
inline?: boolean;
}; };
type RadioProps = { type RadioProps = {
...@@ -106,7 +111,15 @@ type DropdownProps = { ...@@ -106,7 +111,15 @@ type DropdownProps = {
onChange?: (value: number | string) => void; onChange?: (value: number | string) => void;
}; };
export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps | RadioProps | BooleanProps | NumberProps; export type InputProps =
| TextProps
| SliderProps
| CheckboxProps
| DropdownProps
| RadioProps
| BooleanProps
| NumberProps
| ToggleSwitchProps;
export const Input = (props: InputProps) => { export const Input = (props: InputProps) => {
switch (props.type) { switch (props.type) {
...@@ -124,6 +137,8 @@ export const Input = (props: InputProps) => { ...@@ -124,6 +137,8 @@ export const Input = (props: InputProps) => {
return <BooleanInput {...(props as BooleanProps)} />; return <BooleanInput {...(props as BooleanProps)} />;
case 'number': case 'number':
return <NumberInput {...(props as NumberProps)} />; return <NumberInput {...(props as NumberProps)} />;
case 'toggle':
return <ToggleSwitchInput {...(props as ToggleSwitchProps)} />;
default: default:
return null; return null;
} }
...@@ -407,23 +422,31 @@ export const CheckboxInput = ({ label, value, options, onChange, tooltip }: Chec ...@@ -407,23 +422,31 @@ export const CheckboxInput = ({ label, value, options, onChange, tooltip }: Chec
); );
}; };
export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) => { export const BooleanInput = ({ label, value, onChange, tooltip, info, size, required, className }: BooleanProps) => {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger className={className + 'w-full flex justify-between'}>
<label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}> {label && (
<span className="text-sm">{label}</span> <label className="label p-0">
<input <span
type="checkbox" className={`text-${size} text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}
checked={value} >
onChange={(event) => { {label}
if (onChange) { </span>
onChange(event.target.checked); {info && <Info tooltip={info} />}
} </label>
}} )}
className="checkbox checkbox-xs" <input
/> type="checkbox"
</label> checked={value}
onChange={(event) => {
if (onChange) {
onChange(event.target.checked);
}
}}
className="checkbox checkbox-xs"
aria-label={`Toggle ${label}`}
/>
</TooltipTrigger> </TooltipTrigger>
{tooltip && <TooltipContent>{tooltip}</TooltipContent>} {tooltip && <TooltipContent>{tooltip}</TooltipContent>}
</Tooltip> </Tooltip>
...@@ -523,3 +546,42 @@ export const DropdownInput = ({ ...@@ -523,3 +546,42 @@ export const DropdownInput = ({
</Tooltip> </Tooltip>
); );
}; };
type ToggleSwitchProps = {
label: string;
type: 'toggle';
value: boolean;
classText?: string;
tooltip?: string;
onChange?: (value: boolean) => void;
};
export const ToggleSwitchInput = ({ label, classText, value, tooltip, onChange }: ToggleSwitchProps) => {
const [isSelected, setIsSelected] = useState(value);
const handleClick = () => {
const newValue = !isSelected;
setIsSelected(newValue);
if (onChange) {
onChange(newValue);
}
};
return (
<Tooltip>
<TooltipTrigger>
<div className="flex items-center space-x-2">
<div
className={`relative flex w-10 h-5 rounded-full cursor-pointer transition-all duration-500 ${isSelected ? 'bg-secondary-800' : 'bg-secondary-300'}`}
onClick={handleClick}
>
<div
className={`absolute top-0 left-0 w-5 h-5 rounded-full items-center bg-white transition-all duration-500 shadow-lg border-1 border-gray ${isSelected ? 'translate-x-full' : 'translate-x-0'} `}
></div>
</div>
<span className={classText}>{label}</span>
</div>
</TooltipTrigger>
{tooltip && <TooltipContent>{tooltip}</TooltipContent>}
</Tooltip>
);
};
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { ToggleSwitchInput } from '.';
const Component: Meta<typeof ToggleSwitchInput> = {
title: 'Components/Inputs',
component: ToggleSwitchInput,
argTypes: { onChange: {} },
decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>],
};
export default Component;
type Story = StoryObj<typeof Component>;
export const ToggleSwitchInputStory: Story = {
args: {
type: 'toggle',
label: 'Toggle Switch component',
value: false,
onChange: (value) => {},
},
};
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { TableUI } from './TableUI'; // Import the TableUI component from the correct path
const metaTableUI: Meta<typeof TableUI> = {
component: TableUI,
title: 'Components/TableUI',
};
export default metaTableUI;
type Story = StoryObj<typeof TableUI>;
interface SampleData {
name: string;
age: string;
email: string;
role: string;
}
const fieldConfigs = [
{ key: 'name', label: 'Name', type: 'text' as const, required: true },
{ key: 'age', label: 'Age', type: 'text' as const, required: true },
{ key: 'email', label: 'Email', type: 'text' as const, required: true },
{ key: 'role', label: 'Role', type: 'dropdown' as const, options: ['Admin', 'User', 'Guest'] },
];
const dropdownOptions = {
role: ['Admin', 'User', 'Guest'],
};
export const MainStory: Story = {
render: (args) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [tableData, setTableData] = useState<SampleData[]>([
{ name: 'John Doe', age: '28', email: 'j.doe@email.com', role: 'Admin' },
{ name: 'Jane Smith', age: '34', email: 'j.smith@email.com', role: 'User' },
]);
const handleDataChange = (updatedData: SampleData[]) => {
setTableData(updatedData);
};
return <TableUI {...args} data={tableData} onDataChange={handleDataChange} />;
},
args: {
fieldConfigs: fieldConfigs,
dropdownOptions: dropdownOptions,
},
};
import React, { useState } from 'react';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
interface FieldConfig<T> {
key: keyof T;
label: string;
type: 'text' | 'dropdown';
options?: string[];
required?: boolean;
}
interface TableUIProps<T> {
data: T[];
onDataChange: (updatedData: T[]) => void;
fieldConfigs: FieldConfig<T>[];
dropdownOptions: Record<string, string[]>;
}
export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dropdownOptions, onDataChange }: TableUIProps<T>) => {
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editItem, setEditItem] = useState<T | null>(null);
const handleAddRow = () => {
const newItem = {} as T;
fieldConfigs.forEach((config) => {
newItem[config.key] = config.type === 'dropdown' ? ('' as T[keyof T]) : ('' as T[keyof T]);
});
onDataChange([...data, newItem]);
setEditingIndex(data.length);
setEditItem(newItem);
};
const handleDeleteRow = (index: number) => {
const updatedData = data.filter((_, i) => i !== index);
onDataChange(updatedData);
};
const handleEditRow = (index: number) => {
setEditingIndex(index);
setEditItem(data[index]);
};
const handleSaveEdit = () => {
if (editingIndex !== null && editItem) {
const updatedData = data.map((item, index) => (index === editingIndex ? editItem : item));
onDataChange(updatedData);
setEditingIndex(null);
setEditItem(null);
}
};
const handleCancelEdit = () => {
if (editingIndex === data.length - 1) {
onDataChange(data.slice(0, -1));
}
setEditingIndex(null);
setEditItem(null);
};
const handleInputChange = (key: keyof T, value: string) => {
setEditItem((prev) => (prev ? { ...prev, [key]: value } : null));
};
return (
<div className="mt-2 w-full overflow-x-auto">
<div className="flex justify-end mb-4">
<Button variant="solid" size="md" label="Add Row" onClick={handleAddRow} />
</div>
<table className="min-w-full bg-white border border-gray-300 rounded-md">
<thead>
<tr className="bg-gray-100 border-b">
{fieldConfigs.map((field) => (
<th key={field.key.toString()} className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
{field.label}
</th>
))}
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={index} className="border-b rounded-md">
{fieldConfigs.map((field) => (
<td key={field.key.toString()} className="px-6 py-4 text-sm text-gray-700">
{editingIndex === index ? (
field.type === 'dropdown' ? (
<Input
type="dropdown"
label=""
options={dropdownOptions[field.key.toString()] || []}
value={editItem ? editItem[field.key] : ''}
onChange={(value) => handleInputChange(field.key, value.toString())}
/>
) : (
<Input
type="text"
size="xs"
value={editItem ? editItem[field.key] : ''}
required={field.required}
onChange={(e) => handleInputChange(field.key, e)}
/>
)
) : (
item[field.key]
)}
</td>
))}
<td className="px-6 py-4 text-sm text-gray-700">
<div className="flex space-x-2">
{editingIndex === index ? (
<>
<Button variant="solid" size="sm" label="Save" onClick={handleSaveEdit} />
<Button variant="ghost" size="sm" label="Cancel" onClick={handleCancelEdit} />
</>
) : (
<>
<Button
variant="ghost"
size="lg"
iconComponent={'icon-[ic--baseline-delete-outline]'}
className="text-danger-500"
onClick={() => handleDeleteRow(index)}
/>
<Button
variantType="secondary"
variant="ghost"
size="lg"
iconComponent={'icon-[ic--baseline-mode-edit]'}
onClick={() => handleEditRow(index)}
/>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
import React, { useState } from 'react';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { useDialogContext } from '@graphpolaris/shared/lib/components/layout/Dialog';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { TableUI } from '@graphpolaris/shared/lib/components/tableUI/TableUI';
import { useUsersPolicy } from '@graphpolaris/shared/lib/data-access/store';
import { useDispatch } from 'react-redux';
import { setUsersPolicy, UserPolicy } from '@graphpolaris/shared/lib/data-access/store/authorizationUsersSlice';
interface UserManagementContentProps {
sessionId: string;
onConfirm: (users: { name: string; email: string; type: string }[]) => void;
onClickShareLink: () => void;
}
interface FieldConfig<T> {
key: keyof T;
label: string;
type: 'text' | 'dropdown';
required?: boolean;
}
export const UserManagementContent: React.FC<UserManagementContentProps> = ({ sessionId, onConfirm, onClickShareLink }) => {
const { setOpen } = useDialogContext();
const dispatch = useDispatch();
// !FIXME: This should definited at high level
const optionsTypeUser = ['', 'Creator', 'Viewer'];
const [copiedAccessOption, setCopiedAccessOption] = useState<string>(optionsTypeUser[2]);
const usersPolicy = useUsersPolicy();
// !FIXME: This should be populated from the store
const userFields = [
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'email', label: 'Email', type: 'text', required: true },
{ key: 'type', label: 'Type', type: 'dropdown', required: true },
] as FieldConfig<UserPolicy>[];
const options = {
type: optionsTypeUser,
};
const handleUserChange = (newUsers: UserPolicy[]) => {
dispatch(setUsersPolicy({ users: newUsers }));
};
const handleCancel = () => {
setOpen(false);
};
const handleClickShare = () => {
setOpen(false);
onClickShareLink();
};
return (
<div className="flex flex-col w-[50rem] h-[40rem]">
<div className="flex-grow justify-center p-2 text-sm overflow-hidden text-ellipsis whitespace-nowrap">
<h1 className="flex justify-center font-bold gap-x-1">
<span>Manage Users Sharing of</span> <span className="text-secondary-500">{sessionId}</span>
</h1>
</div>
<div className="flex flex-row items-center justify-center gap-2 mt-4 w-full">
<span>By sharing this link recipient will have </span>
<div>
<Input
type="dropdown"
label=""
value={copiedAccessOption}
options={optionsTypeUser}
onChange={(value) => setCopiedAccessOption(value.toString())}
/>
</div>
<span>access</span>
<Button variant="solid" size="md" label="Copy link" onClick={handleClickShare} />
</div>
<div className="flex items-center my-4">
<hr className="flex-grow border-t border-gray-300" />
<span className="mx-4 text-gray-500">or</span>
<hr className="flex-grow border-t border-gray-300" />
</div>
<div className="flex flex-col items-center flex-grow mt-4">
<TableUI data={usersPolicy.users} fieldConfigs={userFields} dropdownOptions={options} onDataChange={handleUserChange} />
</div>
<div className="flex justify-center p-2 mt-auto">
<div className="flex space-x-4">
<Button variant="outline" size="md" label="Cancel" onClick={handleCancel} />
<Button variantType="primary" size="md" label="Confirm" onClick={() => onConfirm(usersPolicy.users)} />
</div>
</div>
</div>
);
};
export * from './useAuth'; export * from './useAuth';
export * from './useResourcesPolicy';
import { useResourcesPolicy } from '@graphpolaris/shared/lib/data-access';
import { useMemo } from 'react';
//import casbinjs from 'casbin.js';
import { Authorizer } from 'casbin.js';
export const useCheckPermissionPolicy = () => {
const policyPermissions = useResourcesPolicy();
const authorizer = useMemo(() => {
// docs tell to go this way, but it doesn't work
//const auth = new casbinjs.Authorizer('manual');
const auth = new Authorizer('manual');
const permission = {
read: policyPermissions.read,
write: policyPermissions.write,
};
auth.setPermission(permission);
return auth;
}, [policyPermissions]);
const canRead = async (resource: string) => {
return await authorizer.can('read', resource);
};
const canWrite = async (resource: string) => {
return await authorizer.can('write', resource);
};
return {
canRead,
canWrite,
};
};
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './store';
export interface PolicyResourcesState {
read: string[];
write: string[];
}
//TODO !FIXME: add middleware to fetch resources from backend
const initialState: PolicyResourcesState = {
read: ['database', 'query', 'visualization', 'policy'],
write: ['database', 'query', 'visualization', 'policy'],
};
export const policyResourcesSlice = createSlice({
name: 'policyResources',
initialState,
reducers: {
getResourcesPolicy: (state, action: PayloadAction<PolicyResourcesState>) => {
return action.payload;
},
},
});
export const { getResourcesPolicy } = policyResourcesSlice.actions;
export default policyResourcesSlice.reducer;
export const selectResourcesPolicyState = (state: RootState) => state.policyResources;
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './store';
export interface UserPolicy {
name: string;
email: string;
type: string;
}
export interface PolicyUsersState {
users: UserPolicy[];
}
//TODO !FIXME: add middleware to fetch users from backend
const initialState: PolicyUsersState = {
users: [
{ name: 'Scheper. W', email: 'user1@companyA.com', type: 'creator' },
{ name: 'Smit. S', email: 'user2@companyB.com', type: 'viewer' },
{ name: 'De Jong. B', email: 'user3@companyC.com', type: 'creator' },
],
};
export const policyUsersSlice = createSlice({
name: 'policyUsers',
initialState,
reducers: {
setUsersPolicy: (state, action: PayloadAction<PolicyUsersState>) => {
return action.payload;
},
},
});
export const { setUsersPolicy } = policyUsersSlice.actions;
export default policyUsersSlice.reducer;
export const selectPolicyState = (state: RootState) => state.policyUsers;
...@@ -38,6 +38,8 @@ import { SchemaGraph, SchemaGraphInference, SchemaGraphStats } from '../../schem ...@@ -38,6 +38,8 @@ import { SchemaGraph, SchemaGraphInference, SchemaGraphStats } from '../../schem
import { GraphMetadata } from '../statistics'; import { GraphMetadata } from '../statistics';
import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice'; import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice';
import { VisualizationSettingsType } from '../../vis/common'; import { VisualizationSettingsType } from '../../vis/common';
import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice';
import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice';
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch: () => AppDispatch = useDispatch;
...@@ -85,3 +87,9 @@ export const useActiveVisualization: () => VisualizationSettingsType | undefined ...@@ -85,3 +87,9 @@ export const useActiveVisualization: () => VisualizationSettingsType | undefined
// Interaction Slices // Interaction Slices
export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState); export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState);
export const useFocus: () => FocusStateI | undefined = () => useAppSelector(focusState); export const useFocus: () => FocusStateI | undefined = () => useAppSelector(focusState);
// Authorization Users Slice
export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selectPolicyState);
// Authorization Resources Slice
export const useResourcesPolicy: () => PolicyResourcesState = () => useAppSelector(selectResourcesPolicyState);
...@@ -9,6 +9,8 @@ import mlSlice from './mlSlice'; ...@@ -9,6 +9,8 @@ import mlSlice from './mlSlice';
import searchResultSlice from './searchResultSlice'; import searchResultSlice from './searchResultSlice';
import visualizationSlice from './visualizationSlice'; import visualizationSlice from './visualizationSlice';
import interactionSlice from './interactionSlice'; import interactionSlice from './interactionSlice';
import policyUsersSlice from './authorizationUsersSlice';
import policyPermissionSlice from './authorizationResourcesSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
...@@ -22,6 +24,8 @@ export const store = configureStore({ ...@@ -22,6 +24,8 @@ export const store = configureStore({
searchResults: searchResultSlice, searchResults: searchResultSlice,
interaction: interactionSlice, interaction: interactionSlice,
visualize: visualizationSlice, visualize: visualizationSlice,
policyUsers: policyUsersSlice,
policyResources: policyPermissionSlice,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
useSchemaInference, useSchemaInference,
useSearchResultQB, useSearchResultQB,
} from '@graphpolaris/shared/lib/data-access/store'; } from '@graphpolaris/shared/lib/data-access/store';
import { useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
...@@ -83,6 +84,28 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { ...@@ -83,6 +84,28 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
const searchResults = useSearchResultQB(); const searchResults = useSearchResultQB();
const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
const [allowZoom, setAllowZoom] = useState(true); const [allowZoom, setAllowZoom] = useState(true);
const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'query';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
useEffect(() => { useEffect(() => {
const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]); const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]);
...@@ -223,6 +246,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { ...@@ -223,6 +246,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
* @param event Drag event. * @param event Drag event.
*/ */
const onDrop = (event: React.DragEvent<HTMLDivElement>): void => { const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
if (!writeAllowed) {
console.debug('User blocked from editing query due to being a viewer');
event.preventDefault();
return;
}
event.preventDefault(); event.preventDefault();
// The dropped element should be a valid reactflow element // The dropped element should be a valid reactflow element
......
import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import { useQuerybuilderGraph, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model'; import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model';
import { PillDropdown } from '../../pilldropdown/PillDropdown'; import { PillDropdown } from '../../pilldropdown/PillDropdown';
...@@ -24,7 +24,28 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { ...@@ -24,7 +24,28 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
); );
const [openDropdown, setOpenDropdown] = useState(false); const [openDropdown, setOpenDropdown] = useState(false);
const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'query';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
return ( return (
<div className="w-fit h-fit nowheel" ref={ref} id="asd"> <div className="w-fit h-fit nowheel" ref={ref} id="asd">
<EntityPill <EntityPill
...@@ -52,6 +73,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { ...@@ -52,6 +73,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
position={Position.Left} position={Position.Left}
className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'} className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
type="target" type="target"
style={{
pointerEvents: writeAllowed ? 'auto' : 'none',
}}
></Handle> ></Handle>
} }
handleRight={ handleRight={
...@@ -61,6 +85,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { ...@@ -61,6 +85,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
position={Position.Right} position={Position.Right}
className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'} className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
type="source" type="source"
style={{
pointerEvents: writeAllowed ? 'auto' : 'none',
}}
></Handle> ></Handle>
} }
> >
......
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components'; import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip';
import { ControlContainer } from '../../components/controls'; import { ControlContainer } from '../../components/controls';
import { Tabs, Tab } from '../../components/tabs'; import { Tabs, Tab } from '../../components/tabs';
import { useAppDispatch, useVisualization } from '../../data-access'; import { useAppDispatch, useVisualization, useCheckPermissionPolicy } from '../../data-access';
import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice'; import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice';
import { Visualizations } from './VisualizationPanel'; import { Visualizations } from './VisualizationPanel';
...@@ -12,6 +12,29 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor ...@@ -12,6 +12,29 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'visualization';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, i: number) => { const handleDragStart = (e: React.DragEvent<HTMLDivElement>, i: number) => {
e.dataTransfer.setData('text/plain', i.toString()); e.dataTransfer.setData('text/plain', i.toString());
}; };
...@@ -90,14 +113,16 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor ...@@ -90,14 +113,16 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<DropdownContainer open={open} onOpenChange={setOpen}> <DropdownContainer open={open} onOpenChange={setOpen}>
<DropdownTrigger onClick={() => setOpen((v) => !v)}> <DropdownTrigger disabled={!writeAllowed} onClick={() => setOpen((v) => !v)}>
<Button <Button
as={'a'} as={'a'}
variantType="secondary" variantType="secondary"
variant="ghost" variant="ghost"
size="xs" size="xs"
disabled={true}
iconComponent="icon-[ic--baseline-add]" iconComponent="icon-[ic--baseline-add]"
onClick={() => {}} onClick={() => {}}
className={`${writeAllowed ? 'cursor-pointer' : 'cursor-not-allowed'}`}
/> />
</DropdownTrigger> </DropdownTrigger>
<DropdownItemContainer> <DropdownItemContainer>
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import Info from '../../components/info'; import Info from '../../components/info';
import { addVisualization } from '../../data-access/store/visualizationSlice'; import { addVisualization } from '../../data-access/store/visualizationSlice';
import { useAppDispatch } from '../../data-access'; import { useAppDispatch, useCheckPermissionPolicy } from '../../data-access';
import { Visualizations } from '../components/VisualizationPanel'; import { Visualizations } from '../components/VisualizationPanel';
type VisualizationDescription = { type VisualizationDescription = {
...@@ -14,6 +14,29 @@ export function Recommender() { ...@@ -14,6 +14,29 @@ export function Recommender() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [visualizationDescriptions, setVisualizationDescriptions] = useState<VisualizationDescription[]>([]); const [visualizationDescriptions, setVisualizationDescriptions] = useState<VisualizationDescription[]>([]);
const { canRead, canWrite } = useCheckPermissionPolicy();
const [readAllowed, setReadAllowed] = useState(false);
const [writeAllowed, setWriteAllowed] = useState(false);
const resource = 'visualization';
const checkReadPermission = useCallback(async () => {
const result = await canRead(resource);
setReadAllowed(result);
}, [canRead]);
const checkWritePermission = useCallback(async () => {
const result = await canWrite(resource);
setWriteAllowed(result);
}, [canWrite]);
useEffect(() => {
checkReadPermission();
}, [checkReadPermission]);
useEffect(() => {
checkWritePermission();
}, [checkWritePermission]);
useEffect(() => { useEffect(() => {
const loadVisualizations = async () => { const loadVisualizations = async () => {
const descriptions = await Promise.all( const descriptions = await Promise.all(
...@@ -40,9 +63,14 @@ export function Recommender() { ...@@ -40,9 +63,14 @@ export function Recommender() {
{visualizationDescriptions.map(({ name, displayName, description }) => ( {visualizationDescriptions.map(({ name, displayName, description }) => (
<div <div
key={name} key={name}
className="p-4 cursor-pointer border hover:bg-secondary-100" className={`p-4 border ${writeAllowed ? 'cursor-pointer hover:bg-secondary-100' : 'cursor-not-allowed opacity-50'}`}
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
if (!writeAllowed) {
console.debug('User blocked from editing query due to being a viewer');
return;
}
const component = await Visualizations[name](); const component = await Visualizations[name]();
dispatch(addVisualization({ ...component.default.settings, name: name, id: name })); dispatch(addVisualization({ ...component.default.settings, name: name, id: name }));
}} }}
......
...@@ -15,10 +15,10 @@ export function ActionBar({ isSearching, setIsSearching, setSearchResult, setSel ...@@ -15,10 +15,10 @@ export function ActionBar({ isSearching, setIsSearching, setSearchResult, setSel
return ( return (
<div> <div>
<div className="absolute left-0 top-0 m-1"> <div className="absolute left-0 top-0 m-1">
<div className="cursor-pointer p-1 pb-0 bg-white shadow-md rounded" onClick={() => setSelectingRectangle(true)}> <div className="cursor-pointer p-1 pb-0 bg-light shadow-md rounded" onClick={() => setSelectingRectangle(true)}>
<Icon component="icon-[ic--baseline-highlight-alt]" /> <Icon component="icon-[ic--baseline-highlight-alt]" />
</div> </div>
<div className="cursor-pointer p-1 mt-1 pb-0 bg-white shadow-md rounded" onClick={() => setIsSearching(!isSearching)}> <div className="cursor-pointer p-1 mt-1 pb-0 bg-light shadow-md rounded" onClick={() => setIsSearching(!isSearching)}>
<Icon component="icon-[ic--outline-search]" /> <Icon component="icon-[ic--outline-search]" />
</div> </div>
</div> </div>
......
...@@ -35,7 +35,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => { ...@@ -35,7 +35,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
}; };
return ( return (
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 z-50 m-1 p-2 bg-white shadow-md rounded w-full max-w-xl"> <div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 z-50 m-1 p-2 bg-light shadow-md rounded w-full max-w-xl">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Input type="text" size="xs" value={query} onChange={(value) => setQuery(value)} /> <Input type="text" size="xs" value={query} onChange={(value) => setQuery(value)} />
<Button label="Search" size="xs" onClick={handleSearch} disabled={isLoading} /> <Button label="Search" size="xs" onClick={handleSearch} disabled={isLoading} />
......