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 { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access';
import React, { useEffect, useState, useCallback } from 'react';
import {
useAppDispatch,
useSchemaGraph,
useSessionCache,
useAuthorizationCache,
useCheckPermissionPolicy,
} from '@graphpolaris/shared/lib/data-access';
import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
import { SettingsForm } from './forms/settings';
import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner';
......@@ -56,6 +62,29 @@ export default function DatabaseSelector({}) {
};
}, [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 (
<div className="menu-walkthrough">
<TooltipProvider delayDuration={1000}>
......@@ -85,11 +114,14 @@ export default function DatabaseSelector({}) {
>
<DropdownTrigger
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]"
size="md"
disabled={connecting || authCache.authorized === false || !!authCache.roomID}
disabled={connecting || authCache.authorized === false || !!authCache.roomID || !writeAllowed}
title={
<div className="flex items-center">
{connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? (
......
......@@ -8,18 +8,24 @@
/* 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.
* See testing plan for more details.*/
import React, { useState, useRef, useEffect } from 'react';
import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useAuthorizationCache, useAuth, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
import DatabaseSelector from './DatabaseManagement/dbConnectionSelector';
import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
import GpLogo from './gp-logo';
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 = () => {
const dropdownRef = useRef<HTMLDivElement>(null);
const auth = useAuth();
const authCache = useAuthorizationCache();
const [menuOpen, setMenuOpen] = useState(false);
const dispatch = useDispatch();
const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
......@@ -33,8 +39,37 @@ export const Navbar = () => {
};
}, [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 (
<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">
......@@ -53,12 +88,12 @@ export const Navbar = () => {
</div>
</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">
<h2 className="font-bold">user: {authCache.username}</h2>
<h3 className="text-xs break-words">session: {authCache.sessionID}</h3>
<h3 className="text-xs break-words">license: Creator</h3>
</div>
{authCache.authorized ? (
<>
<DropdownItem
......@@ -75,7 +110,6 @@ export const Navbar = () => {
<DropdownItem value="Login" onClick={() => {}} />
</>
)}
{authCache?.roomID && (
<div className="p-2 border-b">
<h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3>
......@@ -84,6 +118,20 @@ export const Navbar = () => {
<div className="p-2 border-t">
<h3 className="text-xs">Version: {buildInfo}</h3>
</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>
</Popover>
</div>
......
......@@ -9,7 +9,7 @@ export type VisualizationTooltipProps = {
export const VisualizationTooltip: React.FC<VisualizationTooltipProps> = ({ name, colorHeader, children }) => {
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="left-0 top-0 h-auto w-1.5" style={{ backgroundColor: colorHeader }}></div>
<div className="px-2.5 py-1 truncate flex">
......
......@@ -78,6 +78,11 @@ type BooleanProps = {
value: boolean;
tooltip?: string;
onChange?: (value: boolean) => void;
size?: 'xs' | 'sm' | 'md' | 'xl';
info?: string;
required?: boolean;
className?: string;
inline?: boolean;
};
type RadioProps = {
......@@ -106,7 +111,15 @@ type DropdownProps = {
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) => {
switch (props.type) {
......@@ -124,6 +137,8 @@ export const Input = (props: InputProps) => {
return <BooleanInput {...(props as BooleanProps)} />;
case 'number':
return <NumberInput {...(props as NumberProps)} />;
case 'toggle':
return <ToggleSwitchInput {...(props as ToggleSwitchProps)} />;
default:
return null;
}
......@@ -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 (
<Tooltip>
<TooltipTrigger>
<label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}>
<span className="text-sm">{label}</span>
<input
type="checkbox"
checked={value}
onChange={(event) => {
if (onChange) {
onChange(event.target.checked);
}
}}
className="checkbox checkbox-xs"
/>
</label>
<TooltipTrigger className={className + 'w-full flex justify-between'}>
{label && (
<label className="label p-0">
<span
className={`text-${size} text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}
>
{label}
</span>
{info && <Info tooltip={info} />}
</label>
)}
<input
type="checkbox"
checked={value}
onChange={(event) => {
if (onChange) {
onChange(event.target.checked);
}
}}
className="checkbox checkbox-xs"
aria-label={`Toggle ${label}`}
/>
</TooltipTrigger>
{tooltip && <TooltipContent>{tooltip}</TooltipContent>}
</Tooltip>
......@@ -523,3 +546,42 @@ export const DropdownInput = ({
</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 './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
import { GraphMetadata } from '../statistics';
import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice';
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`
export const useAppDispatch: () => AppDispatch = useDispatch;
......@@ -85,3 +87,9 @@ export const useActiveVisualization: () => VisualizationSettingsType | undefined
// Interaction Slices
export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState);
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';
import searchResultSlice from './searchResultSlice';
import visualizationSlice from './visualizationSlice';
import interactionSlice from './interactionSlice';
import policyUsersSlice from './authorizationUsersSlice';
import policyPermissionSlice from './authorizationResourcesSlice';
export const store = configureStore({
reducer: {
......@@ -22,6 +24,8 @@ export const store = configureStore({
searchResults: searchResultSlice,
interaction: interactionSlice,
visualize: visualizationSlice,
policyUsers: policyUsersSlice,
policyResources: policyPermissionSlice,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
......
......@@ -8,6 +8,7 @@ import {
useSchemaInference,
useSearchResultQB,
} 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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
......@@ -83,6 +84,28 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
const searchResults = useSearchResultQB();
const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
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(() => {
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) => {
* @param event Drag event.
*/
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();
// The dropped element should be a valid reactflow element
......
import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access';
import React, { useMemo, useRef, useState } from 'react';
import { useQuerybuilderGraph, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model';
import { PillDropdown } from '../../pilldropdown/PillDropdown';
......@@ -24,7 +24,28 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
);
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 (
<div className="w-fit h-fit nowheel" ref={ref} id="asd">
<EntityPill
......@@ -52,6 +73,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
position={Position.Left}
className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
type="target"
style={{
pointerEvents: writeAllowed ? 'auto' : 'none',
}}
></Handle>
}
handleRight={
......@@ -61,6 +85,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
position={Position.Right}
className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
type="source"
style={{
pointerEvents: writeAllowed ? 'auto' : 'none',
}}
></Handle>
}
>
......
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip';
import { ControlContainer } from '../../components/controls';
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 { Visualizations } from './VisualizationPanel';
......@@ -12,6 +12,29 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
const [open, setOpen] = useState(false);
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) => {
e.dataTransfer.setData('text/plain', i.toString());
};
......@@ -90,14 +113,16 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
<Tooltip>
<TooltipTrigger>
<DropdownContainer open={open} onOpenChange={setOpen}>
<DropdownTrigger onClick={() => setOpen((v) => !v)}>
<DropdownTrigger disabled={!writeAllowed} onClick={() => setOpen((v) => !v)}>
<Button
as={'a'}
variantType="secondary"
variant="ghost"
size="xs"
disabled={true}
iconComponent="icon-[ic--baseline-add]"
onClick={() => {}}
className={`${writeAllowed ? 'cursor-pointer' : 'cursor-not-allowed'}`}
/>
</DropdownTrigger>
<DropdownItemContainer>
......
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import Info from '../../components/info';
import { addVisualization } from '../../data-access/store/visualizationSlice';
import { useAppDispatch } from '../../data-access';
import { useAppDispatch, useCheckPermissionPolicy } from '../../data-access';
import { Visualizations } from '../components/VisualizationPanel';
type VisualizationDescription = {
......@@ -14,6 +14,29 @@ export function Recommender() {
const dispatch = useAppDispatch();
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(() => {
const loadVisualizations = async () => {
const descriptions = await Promise.all(
......@@ -40,9 +63,14 @@ export function Recommender() {
{visualizationDescriptions.map(({ name, displayName, description }) => (
<div
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) => {
e.preventDefault();
if (!writeAllowed) {
console.debug('User blocked from editing query due to being a viewer');
return;
}
const component = await Visualizations[name]();
dispatch(addVisualization({ ...component.default.settings, name: name, id: name }));
}}
......
......@@ -15,10 +15,10 @@ export function ActionBar({ isSearching, setIsSearching, setSearchResult, setSel
return (
<div>
<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]" />
</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]" />
</div>
</div>
......
......@@ -35,7 +35,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
};
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">
<Input type="text" size="xs" value={query} onChange={(value) => setQuery(value)} />
<Button label="Search" size="xs" onClick={handleSearch} disabled={isLoading} />
......