From b11f32c1ece2d4b8fbd14a6eeeacc441e1da8b3d Mon Sep 17 00:00:00 2001 From: Sjoerd <svink@graphpolaris.com> Date: Sat, 12 Oct 2024 09:34:27 +0000 Subject: [PATCH] feat: rework of project management and permissions screen --- .../dbConnectionSelector.tsx | 258 ------------------ .../DatabaseManagement/forms/settings.tsx | 212 -------------- apps/web/src/components/navbar/gp-logo.tsx | 19 +- apps/web/src/components/navbar/navbar.tsx | 84 ++---- libs/shared/lib/components/buttons/Button.tsx | 21 +- libs/shared/lib/components/icon/index.tsx | 4 +- libs/shared/lib/components/layout/Dialog.tsx | 12 +- .../UserManagementContent.tsx | 96 ------- .../shared/lib/components/tableUI/TableUI.tsx | 3 - libs/shared/lib/components/tabs/Tab.tsx | 57 +++- .../plugins/InsertVariablesPlugin.tsx | 66 ++--- libs/shared/lib/data-access/api/eventBus.tsx | 5 +- .../shared/lib/data-access/broker/wsSchema.ts | 19 +- libs/shared/lib/data-access/store/hooks.ts | 3 + .../lib/data-access/store/schemaSlice.ts | 13 +- libs/shared/lib/management/Explorations.tsx | 45 +++ .../lib/management/ManagementDialog.tsx | 95 +++++++ .../lib/management/ManagementTrigger.tsx | 88 ++++++ libs/shared/lib/management/Members.tsx | 74 +++++ libs/shared/lib/management/Overview.tsx | 35 +++ libs/shared/lib/management/Settings.tsx | 28 ++ libs/shared/lib/management/Workspace.tsx | 39 +++ .../lib/management/database/DatabaseForm.tsx | 5 +- .../lib/management/database/Databases.tsx | 203 ++++++++++++++ .../management/database/MockSaveStates.tsx | 8 +- .../management/database/UpsertDatabase.tsx | 134 +++++++++ libs/shared/lib/management/database/index.ts | 2 + .../management/database/useHandleDatabase.ts | 91 ++++++ libs/shared/lib/management/index.ts | 2 + libs/shared/lib/schema/panel/Schema.tsx | 18 +- .../lib/vis/components/VisualizationPanel.tsx | 2 +- .../vis/components/VisualizationTabBar.tsx | 4 +- 32 files changed, 1024 insertions(+), 721 deletions(-) delete mode 100644 apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx delete mode 100644 apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx delete mode 100644 libs/shared/lib/components/panels/userManagementContent/UserManagementContent.tsx create mode 100644 libs/shared/lib/management/Explorations.tsx create mode 100644 libs/shared/lib/management/ManagementDialog.tsx create mode 100644 libs/shared/lib/management/ManagementTrigger.tsx create mode 100644 libs/shared/lib/management/Members.tsx create mode 100644 libs/shared/lib/management/Overview.tsx create mode 100644 libs/shared/lib/management/Settings.tsx create mode 100644 libs/shared/lib/management/Workspace.tsx rename apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx => libs/shared/lib/management/database/DatabaseForm.tsx (94%) create mode 100644 libs/shared/lib/management/database/Databases.tsx rename apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx => libs/shared/lib/management/database/MockSaveStates.tsx (93%) create mode 100644 libs/shared/lib/management/database/UpsertDatabase.tsx create mode 100644 libs/shared/lib/management/database/index.ts create mode 100644 libs/shared/lib/management/database/useHandleDatabase.ts create mode 100644 libs/shared/lib/management/index.ts diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx deleted file mode 100644 index fabd61ac9..000000000 --- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthCache } 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'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; -import { DropdownTrigger, DropdownContainer, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; -import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { clearSchema } from '@graphpolaris/shared/lib/data-access/store/schemaSlice'; -import { DatabaseStatus, SaveStateI, nilUUID, wsDeleteState } from '@graphpolaris/shared/lib/data-access/broker'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; -import { Icon } from '@graphpolaris/shared'; - -export default function DatabaseSelector({}) { - const dispatch = useAppDispatch(); - const session = useSessionCache(); - const schemaGraph = useSchemaGraph(); - const authCache = useAuthCache(); - const [hovered, setHovered] = useState<string | null>(null); - const [connecting, setConnecting] = useState<boolean>(false); - const [dbSelectionMenuOpen, setDbSelectionMenuOpen] = useState<boolean>(false); - const [settingsMenuOpen, setSettingsMenuOpen] = useState<'add' | 'update' | undefined>(undefined); - const [selectedSaveState, setSelectedSaveState] = useState<SaveStateI | null>(null); - - useEffect(() => { - if ( - (!session.fetchingSaveStates && - session.saveStates && - Object.keys(session.saveStates).length === 0 && - settingsMenuOpen === undefined) || - session.currentSaveState === nilUUID - ) { - setSettingsMenuOpen('add'); - } - }, [session, settingsMenuOpen]); - - useEffect(() => { - setConnecting(false); - }, [schemaGraph]); - - useEffect(() => { - let timeoutId: ReturnType<typeof setTimeout>; - if (connecting) { - timeoutId = setTimeout(() => { - dispatch(addError("Couldn't establish connection")); - setConnecting(false); - dispatch(selectSaveState(undefined)); - dispatch(clearQB()); - dispatch(clearSchema()); - }, 10000); - } - - return () => { - if (timeoutId) clearTimeout(timeoutId); - }; - }, [connecting]); - - return ( - <div className="menu-walkthrough"> - <TooltipProvider delayDuration={1000}> - {settingsMenuOpen !== undefined && authCache.authorization.savestate.W && ( - <SettingsForm - open={settingsMenuOpen} - saveState={settingsMenuOpen === 'update' ? selectedSaveState : null} - disableCancel={ - (session.saveStates && Object.keys(session.saveStates).length === 0) || - session.currentSaveState === '00000000-0000-0000-0000-000000000000' - } - onClose={() => { - setSettingsMenuOpen(undefined); - }} - /> - )} - <DropdownContainer - open={dbSelectionMenuOpen} - onOpenChange={(ret) => { - if (!ret) { - if (session.saveStates && Object.keys(session.saveStates).length === 0) setSettingsMenuOpen('add'); - else setDbSelectionMenuOpen(!dbSelectionMenuOpen); - } else { - setDbSelectionMenuOpen(true); - } - }} - > - <DropdownTrigger - onClick={() => { - if ( - connecting || - authCache.authentication?.authenticated === false || - !!authCache.authentication?.roomID || - authCache.authorization.savestate.W - ) { - setDbSelectionMenuOpen(!dbSelectionMenuOpen); - } - }} - className="w-[18rem]" - size="md" - noDropdownArrow={ - connecting || - authCache.authentication?.authenticated === false || - !!authCache.authentication?.roomID || - !authCache.authorization.savestate.W - } - title={ - <div className="flex items-center"> - {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? ( - <> - <LoadingSpinner /> - <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> - </> - ) : session.currentSaveState && session.currentSaveState in session.saveStates && session.currentSaveState !== nilUUID ? ( - <div className="flex"> - <Icon component="icon-[ic--outline-storage]" size={20} className=" self-center" /> - <span className="relative"> - <span - className={`absolute bottom-0.5 right-3 h-2 w-2 border border-light rounded-full ${session.testedSaveState[session.currentSaveState] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'}`} - /> - </span> - <p className="ml-2 truncate">{session.saveStates[session.currentSaveState].name}</p> - </div> - ) : session.saveStates === undefined ? ( - <> - <LoadingSpinner /> - <p className="ml-2">Retrieving databases</p> - </> - ) : Object.keys(session.saveStates).length === 0 || session.currentSaveState === nilUUID ? ( - <> - <p className="ml-2">Add your first Database</p> - </> - ) : ( - <> - <div className="h-2 w-2 rounded-full bg-secondary-500" /> - <p className="ml-2">Select a database</p> - </> - )} - </div> - } - /> - - {session.saveStates !== undefined && ( - <DropdownItemContainer> - <li - className="flex items-center p-2 hover:bg-secondary-50 cursor-pointer" - onClick={(e) => { - e.preventDefault(); - setDbSelectionMenuOpen(false); - setConnecting(false); - setSettingsMenuOpen('add'); - }} - > - {session.saveStates && Object.keys(session.saveStates).length === 0 ? ( - <> - <Icon component="icon-[ic--baseline-add]" size={24} /> - <p className="ml-2">Add your first database</p> - </> - ) : ( - <> - <Icon component="icon-[ic--baseline-add]" size={24} /> - <p className="ml-2">Add database</p> - </> - )} - </li> - {Object.values(session.saveStates) - .filter((save) => save.id !== nilUUID) - .map((save) => ( - <li - key={save.id} - className="flex justify-between items-center px-4 py-2 hover:bg-primary-100 gap-2 cursor-pointer" - onClick={(e) => { - if (save.id !== session.currentSaveState) { - e.preventDefault(); - setDbSelectionMenuOpen(false); - setConnecting(true); - dispatch(selectSaveState(save.id)); - dispatch(clearQB()); - dispatch(clearSchema()); - } else { - setDbSelectionMenuOpen(false); - } - }} - onMouseEnter={() => setHovered(save.id)} - onMouseLeave={() => setHovered(null)} - > - <Tooltip placement={'left'}> - <TooltipTrigger> - <div - className={`h-[8px] w-[8px] rounded-full shrink-0 ${ - session.testedSaveState[save.id] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' - }`} - /> - </TooltipTrigger> - <TooltipContent> - <p> - {session.testedSaveState[save.id] === DatabaseStatus.tested - ? 'Database connection tested' - : 'Something went wrong when trying to connect'} - </p> - </TooltipContent> - </Tooltip> - <div className="w-full shrink min-w-0 flex flex-col"> - <p className="truncate w-full shrink-0 min-w-0">{save.name}</p> - <p className="bg-light text-2xs text-secondary-500 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-secondary-200 rounded-sm p-0.5"> - {save.db.protocol} - {save.db.url} - </p> - </div> - <div className={`flex items-center ml-2 ${hovered === save.id ? 'display' : 'invisible'}`}> - <div - className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setSettingsMenuOpen('update'); - setSelectedSaveState(save); - }} - > - <Tooltip> - <TooltipTrigger> - <Icon component="icon-[ic--baseline-settings]" size={24} /> - </TooltipTrigger> - <TooltipContent> - <p>Change the connection details</p> - </TooltipContent> - </Tooltip> - </div> - <div - className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setDbSelectionMenuOpen(false); - if (session.currentSaveState === save.id) { - dispatch(clearQB()); - dispatch(clearSchema()); - } - wsDeleteState(save.id); - dispatch(deleteSaveState(save.id)); - }} - > - <Tooltip> - <TooltipTrigger> - <Icon component="icon-[ic--baseline-delete]" size={24} /> - </TooltipTrigger> - <TooltipContent> - <p>Delete the database</p> - </TooltipContent> - </Tooltip> - </div> - </div> - </li> - ))} - </DropdownItemContainer> - )} - </DropdownContainer> - </TooltipProvider> - </div> - ); -} diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx deleted file mode 100644 index 1877544da..000000000 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - useAppDispatch, - SaveStateI, - wsUpdateState, - wsTestDatabaseConnection, - wsCreateState, - useAuthCache, - nilUUID, -} from '@graphpolaris/shared/lib/data-access'; -import { Dialog, DialogContent } from '@graphpolaris/shared/lib/components/layout'; -import { Button } from '@graphpolaris/shared/lib/components/buttons'; -import { useImmer } from 'use-immer'; -import { addSaveState, testedSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; -import { DatabaseForm, INITIAL_SAVE_STATE } from './databaseForm'; -import { SampleDatabaseSelector } from './mockSaveStates'; -import { Icon } from '@graphpolaris/shared'; - -type Connection = { - updating: boolean; - status: null | string; - verified: boolean | null; -}; - -export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; saveState: SaveStateI | null; disableCancel?: boolean }) => { - const dispatch = useAppDispatch(); - const ref = useRef<HTMLDialogElement>(null); - const auth = useAuthCache(); - const [formData, setFormData] = useImmer( - props.saveState && props.open === 'update' ? props.saveState : { ...INITIAL_SAVE_STATE, user_id: auth.authentication?.userID || '' }, - ); - const [hasError, setHasError] = useState(false); - const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false); - const [connection, setConnection] = useState<Connection>({ - updating: false, - status: null, - verified: null, - }); - const formTitle = props.open === 'add' ? 'Add' : 'Update'; - - useEffect(() => { - if (props.saveState && props.open === 'update') { - setFormData(props.saveState); - setSampleDataPanel(null); - } else { - setSampleDataPanel(false); - } - }, [props.saveState]); - - async function handleSubmit(saveStateData?: SaveStateI, forceAdd: boolean = false): Promise<void> { - if (!saveStateData) saveStateData = formData; - setConnection(() => ({ - updating: true, - status: 'Testing database connection', - verified: null, - })); - - wsTestDatabaseConnection(saveStateData.db, (data) => { - if (!saveStateData) { - console.error('formData is null'); - return; - } - if (!auth.authentication) { - console.error('auth is null'); - return; - } - if (saveStateData.user_id !== auth.authentication.userID && auth.authentication.userID) { - console.error('user_id is not equal to auth.userID'); - saveStateData.user_id = auth.authentication.userID; - } - if (data && data.status === 'success') { - setConnection((prevState) => ({ - updating: false, - status: 'Database connection verified', - verified: true, - })); - if (props.open === 'add' || forceAdd) { - wsCreateState(saveStateData, (_data) => { - dispatch(addSaveState(_data)); - dispatch(testedSaveState(_data.id)); - closeDialog(); - }); - } else { - dispatch(testedSaveState(data.saveStateID)); - wsUpdateState(saveStateData, (_data) => { - dispatch(addSaveState(_data)); - closeDialog(); - }); - } - } else { - setConnection((prevState) => ({ - updating: false, - status: 'Database connection test failed', - verified: false, - })); - } - }); - } - - function handlePortChanged(port: string): void { - if (!isNaN(Number(port))) - setFormData((draft) => { - draft.db.port = Number(port); - return draft; - }); - } - - function closeDialog(): void { - setConnection({ - updating: false, - status: null, - verified: null, - }); - setFormData({ ...INITIAL_SAVE_STATE, user_id: auth.authentication?.userID || '' }); - ref.current?.close(); - props.onClose(); - } - - return ( - <Dialog - open={!!props.open} - onOpenChange={(ret) => { - if (!ret) props.onClose; - }} - > - <DialogContent className="lg:min-w-[50rem]"> - <div className="flex justify-between align-center m-2"> - <h2 className="text-xl font-bold">{formTitle} Database Connection</h2> - <div> - {sampleDataPanel === true ? ( - <Button variant="outline" label="Go back" onClick={() => setSampleDataPanel(false)} /> - ) : sampleDataPanel === false ? ( - <> - <h1 className="font-light text-xs">No data?</h1> - <p className="font-light text-sm cursor-pointer underline" onClick={() => setSampleDataPanel(true)}> - Try sample data - </p> - </> - ) : ( - '' - )} - </div> - </div> - - <> - {sampleDataPanel === true ? ( - <SampleDatabaseSelector - onClick={(data) => { - setHasError(false); - handleSubmit({ ...data, user_id: auth.authentication?.userID || '' }); - }} - /> - ) : ( - <DatabaseForm - data={formData} - onChange={(data: SaveStateI, error: boolean) => { - setFormData({ ...data, id: formData.id }); - setHasError(error); - }} - /> - )} - - {!(connection.status === null) && ( - <div className={`flex flex-col justify-center items-center`}> - <div className="flex justify-center items-center"> - {connection.verified === false && <Icon component="icon-[ic--baseline-error-outline]" className="text-secondary-400" />} - <p className="font-light text-sm text-secondary-400 ">{connection.status}</p> - </div> - {connection.verified === null && <progress className="progress w-56"></progress>} - </div> - )} - - <div - className={`pt-4 flex flex-row gap-3 card-actions w-full justify-stretch items-center ${sampleDataPanel === true && 'hidden'}`} - > - <Button - variantType="primary" - className="flex-grow" - label={connection.updating ? (formTitle === 'Add' ? formTitle + 'ing...' : formTitle.slice(0, -1) + 'ing...') : formTitle} - onClick={(event) => { - event.preventDefault(); - handleSubmit(); - }} - disabled={connection.updating || hasError} - /> - {props.open === 'update' && ( - <Button - variantType="secondary" - className="flex-grow" - label={'Clone'} - onClick={(event) => { - handleSubmit({ ...formData, name: formData.name + ' (copy)', id: nilUUID }, true); - }} - disabled={connection.updating || hasError} - /> - )} - <Button - variant="outline" - className="flex-grow" - label="Cancel" - disabled={props.disableCancel} - onClick={(event) => { - event.preventDefault(); - closeDialog(); - }} - /> - </div> - </> - </DialogContent> - </Dialog> - ); -}; diff --git a/apps/web/src/components/navbar/gp-logo.tsx b/apps/web/src/components/navbar/gp-logo.tsx index a63b23ae1..b8b975a51 100644 --- a/apps/web/src/components/navbar/gp-logo.tsx +++ b/apps/web/src/components/navbar/gp-logo.tsx @@ -2,13 +2,14 @@ import React from 'react'; type GpLogoProps = { className?: string; // `className` is optional and is of type string + includeText?: boolean; }; -const GpLogo: React.FC<GpLogoProps> = ({ className = '' }) => { +const GpLogo: React.FC<GpLogoProps> = ({ className = '', includeText = true }) => { return ( <> <svg xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 444 80" + viewBox={`0 0 ${includeText ? '444' : '80'} 80`} fill="none" preserveAspectRatio="xMidYMid meet" className={`gp-logo ${className}`} @@ -38,12 +39,14 @@ const GpLogo: React.FC<GpLogoProps> = ({ className = '' }) => { <path fill="#E29B27" d="m46.8153 55.6051 7.80257-7.80257 7.80257 7.80257-7.80257 7.80257z" /> <path fill="#EAAE2C" d="m39.0128 63.4077 7.80257-7.80257 7.80257 7.80257-7.80257 7.80257z" /> <path fill="#E9BE31" d="m31.2102 71.2102 7.80257-7.80257 7.80257 7.80257-7.80257 7.80257z" /> - <path - fill="currentColor" - fillRule="evenodd" - d="M400.22 23.8855c.639.6389 1.42.9584 2.343.9584.887 0 1.632-.3195 2.236-.9584.639-.6389.958-1.4197.958-2.3426 0-.9228-.319-1.7037-.958-2.3426-.604-.6389-1.349-.9584-2.236-.9584-.923 0-1.704.3195-2.343.9584s-.958 1.4198-.958 2.3426c0 .9229.319 1.7037.958 2.3426Zm4.685 34.8731V29.5824h-4.845v29.1762h4.845Zm12.57-.6388c1.775.7453 3.78 1.118 6.016 1.118 2.095 0 3.94-.3549 5.538-1.0648 1.632-.7454 2.892-1.757 3.78-3.0348.887-1.2778 1.331-2.7153 1.331-4.3125-.036-1.7747-.533-3.2122-1.491-4.3126-.923-1.1003-2.041-1.9167-3.354-2.4491-1.314-.5679-3.017-1.1358-5.111-1.7037-1.669-.4614-2.982-.8696-3.94-1.2245-.923-.3905-1.704-.8874-2.343-1.4908-.603-.6389-.905-1.4375-.905-2.3959 0-1.2068.515-2.1829 1.544-2.9282 1.029-.7454 2.414-1.1181 4.153-1.1181 1.881 0 3.372.4614 4.472 1.3843 1.136.9228 1.757 2.1474 1.864 3.6736h4.844c-.141-2.8395-1.206-5.0757-3.194-6.7084-1.988-1.6327-4.596-2.4491-7.826-2.4491-2.095 0-3.958.3727-5.591 1.1181-1.633.7099-2.893 1.686-3.78 2.9283-.887 1.2423-1.331 2.6088-1.331 4.0995 0 1.8812.479 3.4075 1.438 4.5788.993 1.1358 2.165 2.0054 3.513 2.6088 1.385.5679 3.159 1.1358 5.325 1.7038 2.378.6034 4.135 1.2245 5.271 1.8634 1.135.6034 1.703 1.544 1.703 2.8218 0 1.2423-.532 2.2716-1.597 3.088-1.065.8164-2.52 1.2245-4.366 1.2245-1.987 0-3.602-.4614-4.845-1.3842-1.207-.9229-1.881-2.1119-2.023-3.5672h-5.005c.107 1.7747.675 3.3542 1.704 4.7385 1.029 1.3488 2.431 2.4136 4.206 3.1945Zm-32.431-23.799c.852-1.6683 2.058-2.9638 3.62-3.8867 1.597-.9228 3.532-1.3842 5.803-1.3842v5.0047h-1.277c-5.431 0-8.146 2.946-8.146 8.838v15.866h-4.845V29.5823h4.845v4.7385Zm-40.825 1.9168c-1.207 2.2361-1.81 4.8449-1.81 7.8264 0 2.9461.603 5.5726 1.81 7.8798 1.207 2.3071 2.857 4.0995 4.952 5.3773 2.094 1.2778 4.436 1.9167 7.027 1.9167 2.521 0 4.721-.5501 6.602-1.6505 1.917-1.1358 3.355-2.5555 4.313-4.2593v5.4307h4.898V29.5824h-4.898v5.3241c-.923-1.6682-2.325-3.0525-4.206-4.1528-1.881-1.1003-4.1-1.6505-6.655-1.6505-2.591 0-4.952.6212-7.081 1.8635-2.095 1.2423-3.745 2.9992-4.952 5.2709Zm21.563 2.1296c.887 1.6327 1.331 3.5494 1.331 5.7501 0 2.2006-.444 4.1351-1.331 5.8033-.887 1.6327-2.094 2.8928-3.62 3.7801-1.491.8519-3.142 1.2778-4.952 1.2778s-3.461-.4259-4.951-1.2778c-1.491-.8873-2.68-2.1474-3.568-3.7801-.887-1.6682-1.331-3.6204-1.331-5.8566 0-2.2006.444-4.1173 1.331-5.75.888-1.6328 2.077-2.8751 3.568-3.7269 1.49-.8519 3.141-1.2778 4.951-1.2778 1.81 0 3.461.4437 4.952 1.331 1.526.8519 2.733 2.0942 3.62 3.7269Zm-29.82-19.0073v39.3987h-4.845V19.3599h4.845Zm-33.63 38.0145c2.236 1.2423 4.721 1.8634 7.454 1.8634 2.768 0 5.288-.6211 7.56-1.8634 2.272-1.2423 4.064-2.9993 5.377-5.2709 1.349-2.3071 2.024-4.9692 2.024-7.9862 0-3.017-.657-5.6614-1.97-7.933-1.278-2.2716-3.035-4.0108-5.271-5.2176-2.236-1.2423-4.739-1.8635-7.507-1.8635-2.769 0-5.271.6212-7.507 1.8635-2.236 1.2068-4.011 2.9637-5.324 5.2709-1.278 2.2716-1.917 4.8982-1.917 7.8797 0 3.017.621 5.6791 1.863 7.9862 1.278 2.2716 3.017 4.0286 5.218 5.2709Zm12.352-3.6204c-1.526.8163-3.159 1.2245-4.898 1.2245-1.739 0-3.337-.3904-4.792-1.1713-1.455-.8164-2.627-2.0409-3.514-3.6736-.852-1.6328-1.278-3.6382-1.278-6.0163 0-2.3426.444-4.3303 1.331-5.9631.888-1.6327 2.059-2.8395 3.514-3.6204 1.491-.8163 3.124-1.2245 4.898-1.2245 1.74 0 3.355.4082 4.845 1.2245 1.491.7809 2.698 1.9877 3.621 3.6204.923 1.6328 1.384 3.6205 1.384 5.9631s-.479 4.3303-1.437 5.963c-.923 1.6327-2.148 2.8573-3.674 3.6737Zm-23.645-21.2435c0 3.088-1.064 5.6613-3.194 7.72-2.094 2.0232-5.307 3.0348-9.637 3.0348h-7.134v15.4932h-4.845V21.6493h11.979c4.189 0 7.365 1.0116 9.53 3.0347 2.201 2.0232 3.301 4.632 3.301 7.8265Zm-12.831 6.7617c2.698 0 4.685-.5857 5.963-1.757 1.278-1.1713 1.917-2.8395 1.917-5.0047 0-4.5787-2.627-6.8681-7.88-6.8681h-7.134v13.6298h7.134Zm-25.03-8.9445c-1.74-.9939-3.727-1.4908-5.963-1.4908-1.953 0-3.727.3727-5.324 1.1181-1.598.7099-2.876 1.7037-3.834 2.9815V19.3599h-7.454v39.3987h7.454V42.4667c0-2.3426.586-4.1351 1.757-5.3774 1.207-1.2778 2.84-1.9167 4.898-1.9167 2.023 0 3.621.6389 4.792 1.9167 1.171 1.2423 1.757 3.0348 1.757 5.3774v16.2919h7.454V41.4551c0-2.6621-.497-4.9337-1.491-6.8149-.958-1.9167-2.307-3.3542-4.046-4.3125Zm-51.204 3.1945c.958-1.3488 2.271-2.4669 3.939-3.3542 1.704-.9229 3.639-1.3843 5.804-1.3843 2.52 0 4.792.6212 6.815 1.8634 2.058 1.2423 3.673 3.0171 4.845 5.3242 1.206 2.2716 1.81 4.9159 1.81 7.933 0 3.017-.604 5.6968-1.81 8.0394-1.172 2.3071-2.787 4.0996-4.845 5.3774-2.023 1.2778-4.295 1.9167-6.815 1.9167-2.165 0-4.082-.4437-5.75-1.3311-1.633-.8873-2.964-2.0054-3.993-3.3542v18.2618h-7.454V29.2629h7.454v4.2593Zm15.599 10.3821c0-1.7748-.372-3.301-1.118-4.5788-.71-1.3133-1.668-2.3071-2.875-2.9815-1.171-.6744-2.449-1.0116-3.833-1.0116-1.349 0-2.627.3549-3.834 1.0648-1.171.6744-2.129 1.6682-2.875 2.9815-.709 1.3133-1.064 2.8573-1.064 4.632 0 1.7747.355 3.3187 1.064 4.632.746 1.3133 1.704 2.3249 2.875 3.0348 1.207.6744 2.485 1.0116 3.834 1.0116 1.384 0 2.662-.355 3.833-1.0648 1.207-.7099 2.165-1.7215 2.875-3.0348.746-1.3133 1.118-2.875 1.118-4.6852Zm-59.296-7.933c-1.172 2.3071-1.757 4.9514-1.757 7.933 0 3.017.585 5.6968 1.757 8.0394 1.206 2.3071 2.821 4.0996 4.845 5.3774 2.058 1.2778 4.33 1.9167 6.815 1.9167 2.2 0 4.135-.4437 5.803-1.3311 1.704-.9228 3.035-2.0764 3.993-3.4607v4.3126h7.507V29.2629h-7.507v4.206c-.994-1.3487-2.325-2.4668-3.993-3.3542-1.633-.8873-3.55-1.331-5.75-1.331-2.52 0-4.81.6212-6.868 1.8634-2.024 1.2423-3.639 3.0171-4.845 5.3242Zm20.391 3.4074c.71 1.2778 1.065 2.8218 1.065 4.632 0 1.8102-.355 3.372-1.065 4.6853-.71 1.2778-1.668 2.2716-2.875 2.9815-1.207.6744-2.502 1.0116-3.887 1.0116-1.348 0-2.626-.355-3.833-1.0648-1.171-.7099-2.13-1.7215-2.875-3.0348-.71-1.3488-1.065-2.9105-1.065-4.6852 0-1.7748.355-3.301 1.065-4.5788.745-1.3133 1.704-2.3071 2.875-2.9815 1.171-.6744 2.449-1.0116 3.833-1.0116 1.385 0 2.68.3549 3.887 1.0648 1.207.6744 2.165 1.6682 2.875 2.9815Zm-34.303-5.537c.958-1.5617 2.2-2.7863 3.727-3.6736 1.561-.8874 3.336-1.3311 5.324-1.3311v7.8265h-1.97c-2.343 0-4.117.5502-5.324 1.6505-1.172 1.1003-1.757 3.017-1.757 5.7501v14.6946h-7.454V29.263h7.454v4.5787Zm-25.96-4.6321c1.491.8164 2.663 2.0055 3.514 3.5672h8.572c-1.171-3.6914-3.301-6.5487-6.389-8.5719-3.088-2.0586-6.726-3.088-10.914-3.088-3.55 0-6.78.8164-9.69 2.4491-2.875 1.5973-5.1468 3.8512-6.815 6.7617-1.6328 2.875-2.4491 6.1405-2.4491 9.7964s.8163 6.9214 2.4491 9.7964c1.6682 2.8751 3.94 5.129 6.815 6.7617 2.91 1.5972 6.158 2.3958 9.743 2.3958 3.195 0 6.07-.6566 8.625-1.9699 2.591-1.3488 4.703-3.1235 6.336-5.3241 1.633-2.2007 2.697-4.5965 3.194-7.1876v-6.4955h-20.125v5.6969h12.778c-.568 2.6975-1.757 4.8094-3.567 6.3357-1.81 1.4908-4.135 2.2361-6.975 2.2361-2.307 0-4.33-.4969-6.069-1.4907-1.739-.9939-3.106-2.4136-4.1-4.2593-.958-1.8457-1.437-4.0109-1.437-6.4955 0-2.4136.479-4.5432 1.437-6.389.958-1.8456 2.29-3.2654 3.993-4.2593 1.704-.9938 3.656-1.4907 5.857-1.4907 1.987 0 3.727.4082 5.217 1.2245Z" - clipRule="evenodd" - /> + {includeText && ( + <path + fill="currentColor" + fillRule="evenodd" + d="M400.22 23.8855c.639.6389 1.42.9584 2.343.9584.887 0 1.632-.3195 2.236-.9584.639-.6389.958-1.4197.958-2.3426 0-.9228-.319-1.7037-.958-2.3426-.604-.6389-1.349-.9584-2.236-.9584-.923 0-1.704.3195-2.343.9584s-.958 1.4198-.958 2.3426c0 .9229.319 1.7037.958 2.3426Zm4.685 34.8731V29.5824h-4.845v29.1762h4.845Zm12.57-.6388c1.775.7453 3.78 1.118 6.016 1.118 2.095 0 3.94-.3549 5.538-1.0648 1.632-.7454 2.892-1.757 3.78-3.0348.887-1.2778 1.331-2.7153 1.331-4.3125-.036-1.7747-.533-3.2122-1.491-4.3126-.923-1.1003-2.041-1.9167-3.354-2.4491-1.314-.5679-3.017-1.1358-5.111-1.7037-1.669-.4614-2.982-.8696-3.94-1.2245-.923-.3905-1.704-.8874-2.343-1.4908-.603-.6389-.905-1.4375-.905-2.3959 0-1.2068.515-2.1829 1.544-2.9282 1.029-.7454 2.414-1.1181 4.153-1.1181 1.881 0 3.372.4614 4.472 1.3843 1.136.9228 1.757 2.1474 1.864 3.6736h4.844c-.141-2.8395-1.206-5.0757-3.194-6.7084-1.988-1.6327-4.596-2.4491-7.826-2.4491-2.095 0-3.958.3727-5.591 1.1181-1.633.7099-2.893 1.686-3.78 2.9283-.887 1.2423-1.331 2.6088-1.331 4.0995 0 1.8812.479 3.4075 1.438 4.5788.993 1.1358 2.165 2.0054 3.513 2.6088 1.385.5679 3.159 1.1358 5.325 1.7038 2.378.6034 4.135 1.2245 5.271 1.8634 1.135.6034 1.703 1.544 1.703 2.8218 0 1.2423-.532 2.2716-1.597 3.088-1.065.8164-2.52 1.2245-4.366 1.2245-1.987 0-3.602-.4614-4.845-1.3842-1.207-.9229-1.881-2.1119-2.023-3.5672h-5.005c.107 1.7747.675 3.3542 1.704 4.7385 1.029 1.3488 2.431 2.4136 4.206 3.1945Zm-32.431-23.799c.852-1.6683 2.058-2.9638 3.62-3.8867 1.597-.9228 3.532-1.3842 5.803-1.3842v5.0047h-1.277c-5.431 0-8.146 2.946-8.146 8.838v15.866h-4.845V29.5823h4.845v4.7385Zm-40.825 1.9168c-1.207 2.2361-1.81 4.8449-1.81 7.8264 0 2.9461.603 5.5726 1.81 7.8798 1.207 2.3071 2.857 4.0995 4.952 5.3773 2.094 1.2778 4.436 1.9167 7.027 1.9167 2.521 0 4.721-.5501 6.602-1.6505 1.917-1.1358 3.355-2.5555 4.313-4.2593v5.4307h4.898V29.5824h-4.898v5.3241c-.923-1.6682-2.325-3.0525-4.206-4.1528-1.881-1.1003-4.1-1.6505-6.655-1.6505-2.591 0-4.952.6212-7.081 1.8635-2.095 1.2423-3.745 2.9992-4.952 5.2709Zm21.563 2.1296c.887 1.6327 1.331 3.5494 1.331 5.7501 0 2.2006-.444 4.1351-1.331 5.8033-.887 1.6327-2.094 2.8928-3.62 3.7801-1.491.8519-3.142 1.2778-4.952 1.2778s-3.461-.4259-4.951-1.2778c-1.491-.8873-2.68-2.1474-3.568-3.7801-.887-1.6682-1.331-3.6204-1.331-5.8566 0-2.2006.444-4.1173 1.331-5.75.888-1.6328 2.077-2.8751 3.568-3.7269 1.49-.8519 3.141-1.2778 4.951-1.2778 1.81 0 3.461.4437 4.952 1.331 1.526.8519 2.733 2.0942 3.62 3.7269Zm-29.82-19.0073v39.3987h-4.845V19.3599h4.845Zm-33.63 38.0145c2.236 1.2423 4.721 1.8634 7.454 1.8634 2.768 0 5.288-.6211 7.56-1.8634 2.272-1.2423 4.064-2.9993 5.377-5.2709 1.349-2.3071 2.024-4.9692 2.024-7.9862 0-3.017-.657-5.6614-1.97-7.933-1.278-2.2716-3.035-4.0108-5.271-5.2176-2.236-1.2423-4.739-1.8635-7.507-1.8635-2.769 0-5.271.6212-7.507 1.8635-2.236 1.2068-4.011 2.9637-5.324 5.2709-1.278 2.2716-1.917 4.8982-1.917 7.8797 0 3.017.621 5.6791 1.863 7.9862 1.278 2.2716 3.017 4.0286 5.218 5.2709Zm12.352-3.6204c-1.526.8163-3.159 1.2245-4.898 1.2245-1.739 0-3.337-.3904-4.792-1.1713-1.455-.8164-2.627-2.0409-3.514-3.6736-.852-1.6328-1.278-3.6382-1.278-6.0163 0-2.3426.444-4.3303 1.331-5.9631.888-1.6327 2.059-2.8395 3.514-3.6204 1.491-.8163 3.124-1.2245 4.898-1.2245 1.74 0 3.355.4082 4.845 1.2245 1.491.7809 2.698 1.9877 3.621 3.6204.923 1.6328 1.384 3.6205 1.384 5.9631s-.479 4.3303-1.437 5.963c-.923 1.6327-2.148 2.8573-3.674 3.6737Zm-23.645-21.2435c0 3.088-1.064 5.6613-3.194 7.72-2.094 2.0232-5.307 3.0348-9.637 3.0348h-7.134v15.4932h-4.845V21.6493h11.979c4.189 0 7.365 1.0116 9.53 3.0347 2.201 2.0232 3.301 4.632 3.301 7.8265Zm-12.831 6.7617c2.698 0 4.685-.5857 5.963-1.757 1.278-1.1713 1.917-2.8395 1.917-5.0047 0-4.5787-2.627-6.8681-7.88-6.8681h-7.134v13.6298h7.134Zm-25.03-8.9445c-1.74-.9939-3.727-1.4908-5.963-1.4908-1.953 0-3.727.3727-5.324 1.1181-1.598.7099-2.876 1.7037-3.834 2.9815V19.3599h-7.454v39.3987h7.454V42.4667c0-2.3426.586-4.1351 1.757-5.3774 1.207-1.2778 2.84-1.9167 4.898-1.9167 2.023 0 3.621.6389 4.792 1.9167 1.171 1.2423 1.757 3.0348 1.757 5.3774v16.2919h7.454V41.4551c0-2.6621-.497-4.9337-1.491-6.8149-.958-1.9167-2.307-3.3542-4.046-4.3125Zm-51.204 3.1945c.958-1.3488 2.271-2.4669 3.939-3.3542 1.704-.9229 3.639-1.3843 5.804-1.3843 2.52 0 4.792.6212 6.815 1.8634 2.058 1.2423 3.673 3.0171 4.845 5.3242 1.206 2.2716 1.81 4.9159 1.81 7.933 0 3.017-.604 5.6968-1.81 8.0394-1.172 2.3071-2.787 4.0996-4.845 5.3774-2.023 1.2778-4.295 1.9167-6.815 1.9167-2.165 0-4.082-.4437-5.75-1.3311-1.633-.8873-2.964-2.0054-3.993-3.3542v18.2618h-7.454V29.2629h7.454v4.2593Zm15.599 10.3821c0-1.7748-.372-3.301-1.118-4.5788-.71-1.3133-1.668-2.3071-2.875-2.9815-1.171-.6744-2.449-1.0116-3.833-1.0116-1.349 0-2.627.3549-3.834 1.0648-1.171.6744-2.129 1.6682-2.875 2.9815-.709 1.3133-1.064 2.8573-1.064 4.632 0 1.7747.355 3.3187 1.064 4.632.746 1.3133 1.704 2.3249 2.875 3.0348 1.207.6744 2.485 1.0116 3.834 1.0116 1.384 0 2.662-.355 3.833-1.0648 1.207-.7099 2.165-1.7215 2.875-3.0348.746-1.3133 1.118-2.875 1.118-4.6852Zm-59.296-7.933c-1.172 2.3071-1.757 4.9514-1.757 7.933 0 3.017.585 5.6968 1.757 8.0394 1.206 2.3071 2.821 4.0996 4.845 5.3774 2.058 1.2778 4.33 1.9167 6.815 1.9167 2.2 0 4.135-.4437 5.803-1.3311 1.704-.9228 3.035-2.0764 3.993-3.4607v4.3126h7.507V29.2629h-7.507v4.206c-.994-1.3487-2.325-2.4668-3.993-3.3542-1.633-.8873-3.55-1.331-5.75-1.331-2.52 0-4.81.6212-6.868 1.8634-2.024 1.2423-3.639 3.0171-4.845 5.3242Zm20.391 3.4074c.71 1.2778 1.065 2.8218 1.065 4.632 0 1.8102-.355 3.372-1.065 4.6853-.71 1.2778-1.668 2.2716-2.875 2.9815-1.207.6744-2.502 1.0116-3.887 1.0116-1.348 0-2.626-.355-3.833-1.0648-1.171-.7099-2.13-1.7215-2.875-3.0348-.71-1.3488-1.065-2.9105-1.065-4.6852 0-1.7748.355-3.301 1.065-4.5788.745-1.3133 1.704-2.3071 2.875-2.9815 1.171-.6744 2.449-1.0116 3.833-1.0116 1.385 0 2.68.3549 3.887 1.0648 1.207.6744 2.165 1.6682 2.875 2.9815Zm-34.303-5.537c.958-1.5617 2.2-2.7863 3.727-3.6736 1.561-.8874 3.336-1.3311 5.324-1.3311v7.8265h-1.97c-2.343 0-4.117.5502-5.324 1.6505-1.172 1.1003-1.757 3.017-1.757 5.7501v14.6946h-7.454V29.263h7.454v4.5787Zm-25.96-4.6321c1.491.8164 2.663 2.0055 3.514 3.5672h8.572c-1.171-3.6914-3.301-6.5487-6.389-8.5719-3.088-2.0586-6.726-3.088-10.914-3.088-3.55 0-6.78.8164-9.69 2.4491-2.875 1.5973-5.1468 3.8512-6.815 6.7617-1.6328 2.875-2.4491 6.1405-2.4491 9.7964s.8163 6.9214 2.4491 9.7964c1.6682 2.8751 3.94 5.129 6.815 6.7617 2.91 1.5972 6.158 2.3958 9.743 2.3958 3.195 0 6.07-.6566 8.625-1.9699 2.591-1.3488 4.703-3.1235 6.336-5.3241 1.633-2.2007 2.697-4.5965 3.194-7.1876v-6.4955h-20.125v5.6969h12.778c-.568 2.6975-1.757 4.8094-3.567 6.3357-1.81 1.4908-4.135 2.2361-6.975 2.2361-2.307 0-4.33-.4969-6.069-1.4907-1.739-.9939-3.106-2.4136-4.1-4.2593-.958-1.8457-1.437-4.0109-1.437-6.4955 0-2.4136.479-4.5432 1.437-6.389.958-1.8456 2.29-3.2654 3.993-4.2593 1.704-.9938 3.656-1.4907 5.857-1.4907 1.987 0 3.727.4082 5.217 1.2245Z" + clipRule="evenodd" + /> + )} </svg> </> ); diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index 435a06eca..e5512cb2c 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -10,23 +10,24 @@ * See testing plan for more details.*/ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useAuthCache, useAuthentication } 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/panels/userManagementContent/UserManagementContent'; -import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { showManagePermissions, showSharableExploration } from 'config'; +import { Button, Dialog, DialogContent, DialogTrigger, useActiveSaveStateAuthorization, useSessionCache } from '@graphpolaris/shared'; +import { ManagementTrigger, ManagementViews } from '@graphpolaris/shared/lib/management'; +import { Members } from '@graphpolaris/shared/lib/management/Members'; export const Navbar = () => { const dropdownRef = useRef<HTMLDivElement>(null); const auth = useAuthentication(); const authCache = useAuthCache(); + const authorization = useActiveSaveStateAuthorization(); const [menuOpen, setMenuOpen] = useState(false); - const dispatch = useDispatch(); const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; + const [managementOpen, setManagementOpen] = useState<boolean>(false); + const [current, setCurrent] = useState<ManagementViews>('overview'); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -40,43 +41,12 @@ export const Navbar = () => { }; }, [menuOpen]); - // 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"> - <GpLogo className="h-7" /> - </a> - <DatabaseSelector /> + <nav className="w-full px-2 h-12 flex flex-row items-center gap-2 md:gap-3 lg:gap-4"> + <GpLogo className="h-7" includeText={false} /> + <ManagementTrigger managementOpen={managementOpen} setManagementOpen={setManagementOpen} current={current} setCurrent={setCurrent} /> + + <Button label="Share" variantType="primary" size="sm" onClick={() => auth.newShareRoom()} /> <div className="ml-auto"> <div className="w-fit" ref={dropdownRef}> <Popover> @@ -105,10 +75,22 @@ export const Navbar = () => { }} /> )} + {authCache.authorization?.savestate?.W && authorization.database.W && ( + <DropdownItem + value="Viewer Permissions" + onClick={() => { + setManagementOpen(true); + setCurrent('members'); + }} + /> + )} <DropdownItem value="Settings" onClick={() => {}} /> - <DropdownItem value="Log out" onClick={() => { - location.replace(`${import.meta.env['GP_AUTH_URL']}/flows/-/default/invalidation/`) - }} /> + <DropdownItem + value="Log out" + onClick={() => { + location.replace(`${import.meta.env['GP_AUTH_URL']}/flows/-/default/invalidation/`); + }} + /> </> ) : ( <> @@ -123,20 +105,6 @@ export const Navbar = () => { <div className="p-2 border-t"> <h3 className="text-xs">Version: {buildInfo}</h3> </div> - {showManagePermissions() && writeAllowed && ( - <> - <Dialog> - <DialogTrigger className="ml-2 text-sm hover:bg-secondary-200">Manage Viewers Permission</DialogTrigger> - <DialogContent> - <UserManagementContent - sessionId={authCache.authentication?.sessionID ?? ''} - onConfirm={handleConfirmUsers} - onClickShareLink={handleClickShareLink} - /> - </DialogContent> - </Dialog> - </> - )} </PopoverContent> </Popover> </div> diff --git a/libs/shared/lib/components/buttons/Button.tsx b/libs/shared/lib/components/buttons/Button.tsx index 639e7f9c6..3d7df746c 100644 --- a/libs/shared/lib/components/buttons/Button.tsx +++ b/libs/shared/lib/components/buttons/Button.tsx @@ -3,7 +3,7 @@ import styles from './buttons.module.scss'; import { Icon } from '../icon'; import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; -type ButtonProps = { +export type ButtonProps = { as?: 'button' | 'a' | 'div'; variantType?: 'primary' | 'secondary' | 'danger'; variant?: 'solid' | 'outline' | 'ghost'; @@ -33,6 +33,25 @@ type ButtonProps = { onMouseLeaveCapture?: (e: any) => void; onMouseOverCapture?: (e: any) => void; onMouseOutCapture?: (e: any) => void; + + // Drag and Drop + onDragStart?: (e: any) => void; + onDrag?: (e: any) => void; + onDragEnd?: (e: any) => void; + onDragEnter?: (e: any) => void; + onDragExit?: (e: any) => void; + onDragLeave?: (e: any) => void; + onDragOver?: (e: any) => void; + onDragStartCapture?: (e: any) => void; + onDragCapture?: (e: any) => void; + onDragEndCapture?: (e: any) => void; + onDragEnterCapture?: (e: any) => void; + onDragExitCapture?: (e: any) => void; + onDragLeaveCapture?: (e: any) => void; + onDragOverCapture?: (e: any) => void; + onDrop?: (e: any) => void; + onDropCapture?: (e: any) => void; + draggable?: boolean; }; export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLDivElement, ButtonProps>( diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx index 67b824fcc..f208efb30 100644 --- a/libs/shared/lib/components/icon/index.tsx +++ b/libs/shared/lib/components/icon/index.tsx @@ -2,8 +2,8 @@ import React, { ReactElement, ReactNode } from 'react'; import { SVGProps } from 'react'; // Define Sizes and IconProps types -export type Sizes = 8 | 10 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40; -export const sizesArray: Sizes[] = [8, 10, 12, 14, 16, 20, 24, 28, 32, 36, 40]; +export const sizesArray = [8, 10, 12, 14, 16, 20, 24, 28, 32, 36, 40, 48, 56] as const; +export type Sizes = (typeof sizesArray)[number]; export type IconProps = SVGProps<SVGSVGElement> & { component?: ReactNode | ReactElement<any> | string; diff --git a/libs/shared/lib/components/layout/Dialog.tsx b/libs/shared/lib/components/layout/Dialog.tsx index 1f6bb731e..f18234621 100644 --- a/libs/shared/lib/components/layout/Dialog.tsx +++ b/libs/shared/lib/components/layout/Dialog.tsx @@ -11,7 +11,7 @@ import { FloatingOverlay, useId, } from '@floating-ui/react'; -import { Button } from '../buttons'; +import { Button, ButtonProps } from '../buttons'; interface DialogOptions { initialOpen?: boolean; @@ -200,9 +200,7 @@ export const DialogDescription = React.forwardRef<HTMLParagraphElement, React.HT ); }); -export const DialogClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>( - function DialogClose(props, ref) { - const { setOpen } = useDialogContext(); - return <Button variant="solid" className="w-full" {...props} ref={ref} onClick={() => setOpen(false)} />; - }, -); +export const DialogClose = React.forwardRef<HTMLButtonElement, ButtonProps>(function DialogClose(props, ref) { + const { setOpen } = useDialogContext(); + return <Button variant="solid" className="w-full" {...props} ref={ref} onClick={() => setOpen(false)} />; +}); diff --git a/libs/shared/lib/components/panels/userManagementContent/UserManagementContent.tsx b/libs/shared/lib/components/panels/userManagementContent/UserManagementContent.tsx deleted file mode 100644 index 7357f76f1..000000000 --- a/libs/shared/lib/components/panels/userManagementContent/UserManagementContent.tsx +++ /dev/null @@ -1,96 +0,0 @@ -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> - ); -}; diff --git a/libs/shared/lib/components/tableUI/TableUI.tsx b/libs/shared/lib/components/tableUI/TableUI.tsx index 8138d147c..02a1d4c8d 100644 --- a/libs/shared/lib/components/tableUI/TableUI.tsx +++ b/libs/shared/lib/components/tableUI/TableUI.tsx @@ -64,9 +64,6 @@ export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dro 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"> diff --git a/libs/shared/lib/components/tabs/Tab.tsx b/libs/shared/lib/components/tabs/Tab.tsx index d007b5c26..49a66667f 100644 --- a/libs/shared/lib/components/tabs/Tab.tsx +++ b/libs/shared/lib/components/tabs/Tab.tsx @@ -1,29 +1,62 @@ import React from 'react'; +import { Button, ButtonProps } from '../buttons'; -export const Tabs = (props: { children: React.ReactNode }) => { +type TabTypes = 'inline' | 'rounded' | 'simple'; + +type ContextType = { + tabType: TabTypes; +}; +const TabContext = React.createContext<ContextType>({ + tabType: 'inline', +}); + +export const Tabs = (props: { children: React.ReactNode; tabType?: TabTypes }) => { + const tabType = props.tabType || 'inline'; + let className = ''; + if (tabType === 'inline') { + className = 'flex items-stretch divide-x divide-secondary-200 border-x border-secondary-200 overflow-x-auto -my-px'; + } else if (tabType === 'rounded') { + className = 'flex gap-x-1 border-b border-secondary-300 mb-5'; + } else if (tabType === 'simple') { + className = 'flex gap-x-1'; + } return ( - <div className="flex items-stretch divide-x divide-secondary-200 border-x border-secondary-200 overflow-x-auto -my-px"> - {props.children} - </div> + <TabContext.Provider value={{ tabType: tabType }}> + <div className={className}>{props.children}</div> + </TabContext.Provider> ); }; export const Tab = ({ activeTab, text, + variant = 'ghost', + className = '', ...props -}: React.ButtonHTMLAttributes<HTMLDivElement> & { +}: ButtonProps & { activeTab: boolean; - children: React.ReactNode; + children?: React.ReactNode; text: string; }) => { + const context = React.useContext(TabContext); + + if (context.tabType === 'inline') { + className += ` pl-2 pr-1 gap-1 relative h-full text-secondary-500 border-secondary-200 before:content-[''] + before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full + ${activeTab ? 'before:bg-primary-500' : 'before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200'}`; + } else if (context.tabType === 'rounded') { + className += ` -mb-px py-4 px-4 text-sm text-secondary-500 text-center border rounded-t-lg rounded-b-none + ${activeTab ? 'active text-secondary-950 border-l-secondary-300 border-r-secondary-300 border-t-secondary-300 border-b-white' : ''} + before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200`; + } else if (context.tabType === 'simple') { + className += ` ${activeTab ? 'active bg-secondary-200 hover:bg-secondary-300' : 'hover:bg-secondary-100'}`; + } + return ( - <div - className={`flex items-center pl-2 pr-1 gap-1 cursor-pointer relative border-secondary-200 before:content-[''] before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full ${activeTab ? 'before:bg-primary-500' : 'before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200'}`} - {...props} - > - <p className={`text-xs text-secondary-500 font-semibold ${activeTab && 'text-secondary-950'}`}>{text}</p> + <Button className={className} label={text} variant={variant} size="xs" {...props}> + {/* <p className={`text-xs font-semibold ${activeTab && 'text-secondary-950'}`}>{text}</p> */} + {props.children} - </div> + </Button> ); }; diff --git a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx index 8e1488014..eebd254d9 100644 --- a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx @@ -1,32 +1,21 @@ -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $copyNode, - $getRoot, - $isParagraphNode, - $isElementNode, - $getNodeByKey, - $getSelection, - type BaseSelection, - $parseSerializedNode -} from "lexical"; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getSelection, type BaseSelection } from 'lexical'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { VariableNode, VariableType } from '../VariableNode'; -import { useState } from 'react'; import { useGraphQueryResult, useVisualization } from '@graphpolaris/shared/lib/data-access'; export const InsertVariablesPlugin = () => { const [editor] = useLexicalComposerContext(); const { openVisualizationArray } = useVisualization(); - const visualizationOptions = openVisualizationArray - .map(x => x.name); + const visualizationOptions = openVisualizationArray.map((x) => x.name); const onChange = (value: string | number, type: VariableType) => { editor.update(() => { const selection = $getSelection() as BaseSelection; - + const node = new VariableNode(String(value), type); - + selection.insertNodes([node]); // TODO: enable drag and dropping nodes @@ -37,34 +26,39 @@ export const InsertVariablesPlugin = () => { const nodeTypes = Object.keys(result.metaData?.nodes.types || {}); function optionsForType(type: string) { - const typeObj = result.metaData?.nodes.types[type] ?? {attributes: null}; - + const typeObj = result.metaData?.nodes.types[type] ?? { attributes: null }; + if (!('attributes' in typeObj) || typeObj.attributes == null) { return []; } - - return Object.entries(typeObj.attributes).map(([k,v]) => Object.keys(v).map(x => `${k}_${x}`)).flat(); + return Object.entries(typeObj.attributes) + .map(([k, v]) => Object.keys(v).map((x) => `${k}_${x}`)) + .flat(); } - return <> - { nodeTypes.map((nodeType) => - <Input - type="dropdown" - label={`${nodeType} variable`} - value="" - options={optionsForType(nodeType)} - onChange={(v) => onChange(v, VariableType.statistic)} - /> - ) - } - { (visualizationOptions.length > 0) ? <Input + return ( + <> + {nodeTypes.map((nodeType) => ( + <Input + type="dropdown" + label={`${nodeType} variable`} + value="" + options={optionsForType(nodeType)} + onChange={(v) => onChange(v, VariableType.statistic)} + /> + ))} + {visualizationOptions.length > 0 ? ( + <Input type="dropdown" label={`Visualization`} value="" options={visualizationOptions} onChange={(v) => onChange(v, VariableType.visualization)} - /> : '' - } - </>; -}; \ No newline at end of file + /> + ) : ( + '' + )} + </> + ); +}; diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx index 283f07dd8..d783b723d 100644 --- a/libs/shared/lib/data-access/api/eventBus.tsx +++ b/libs/shared/lib/data-access/api/eventBus.tsx @@ -46,7 +46,7 @@ import { import { URLParams, getParam, deleteParam } from './url'; import { VisState, setVisualizationState } from '../store/visualizationSlice'; import { isEqual } from 'lodash-es'; -import { setSchemaAttributeDimensions, setSchemaAttributeInformation } from '../store/schemaSlice'; +import { setSchemaAttributeDimensions, setSchemaAttributeInformation, setSchemaLoading } from '../store/schemaSlice'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { unSelect } from '../store/interactionSlice'; import { SchemaGraphStats } from '../../schema'; @@ -90,6 +90,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } unsubs.push( wsSchemaSubscription((data) => { + dispatch(setSchemaLoading(false)); dispatch(readInSchemaFromBackend(data)); dispatch(addInfo('Schema graph updated')); }), @@ -151,6 +152,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } unsubs.push( wsGetStateSubscription((data) => { if (data.id !== nilUUID) { + console.debug('Save State updated', data); dispatch(addSaveState(data)); dispatch(selectSaveState(data.id)); loadSaveState(data.id, session.saveStates); @@ -223,6 +225,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } useEffect(() => { // New active database if (session.currentSaveState && session.currentSaveState !== nilUUID) { + dispatch(setSchemaLoading(true)); wsSchemaRequest(session.currentSaveState); wsSchemaStatsRequest(session.currentSaveState); wsSelectState(session.currentSaveState); diff --git a/libs/shared/lib/data-access/broker/wsSchema.ts b/libs/shared/lib/data-access/broker/wsSchema.ts index 09a81f21a..bfbb2e5eb 100644 --- a/libs/shared/lib/data-access/broker/wsSchema.ts +++ b/libs/shared/lib/data-access/broker/wsSchema.ts @@ -3,15 +3,18 @@ import { SchemaFromBackend, SchemaStatsFromBackend } from '../../schema'; import { Broker } from './broker'; -export function wsSchemaRequest(saveStateID: string) { - Broker.instance().sendMessage({ - key: 'schema', - subKey: 'get', - body: { - cached: false, - saveStateID: saveStateID, +export function wsSchemaRequest(saveStateID: string, callback?: Function) { + Broker.instance().sendMessage( + { + key: 'schema', + subKey: 'get', + body: { + cached: false, + saveStateID: saveStateID, + }, }, - }); + callback, + ); } type SchemaResponse = (data: SchemaFromBackend) => void; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 2c57d5045..d55cbb791 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -7,6 +7,8 @@ import { SchemaSettings, schemaStatsState, schemaInferenceState, + SchemaSliceI, + schema, } from './schemaSlice'; import type { RootState, AppDispatch } from './store'; import { ConfigStateI, configState } from '@graphpolaris/shared/lib/data-access/store/configSlice'; @@ -52,6 +54,7 @@ export const useGraphQueryResult: () => GraphQueryResult = () => useAppSelector( export const useGraphQueryResultMeta: () => GraphMetadata | undefined = () => useAppSelector(selectGraphQueryResultMetaData); // Gives the schema +export const useSchema: () => SchemaSliceI = () => useAppSelector(schema); export const useSchemaGraph: () => SchemaGraph = () => useAppSelector(schemaGraph); export const useSchemaSettings: () => SchemaSettings = () => useAppSelector(schemaSettingsState); export const useSchemaStats: () => SchemaGraphStats = () => useAppSelector(schemaStatsState); diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index 479cfe24e..5c79edddf 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -17,15 +17,16 @@ export type SchemaSettings = { schemaViewState: SchemaViewState; }; -type schemaSliceI = { +export type SchemaSliceI = { graph: SchemaGraph; graphInference: SchemaGraphInference; graphStats: SchemaGraphStats; + loading: boolean; settings: SchemaSettings; }; // Define the initial state using that type -export const initialState: schemaSliceI = { +export const initialState: SchemaSliceI = { graph: new SchemaGraphology().export(), graphInference: { nodes: {}, @@ -41,6 +42,7 @@ export const initialState: schemaSliceI = { stats: {}, }, }, + loading: true, // layoutName: 'Cytoscape_fcose', settings: { connectionType: 'connection', @@ -79,6 +81,11 @@ export const schemaSlice = createSlice({ setSchemaAttributeInformation: (state, action: PayloadAction<SchemaGraphStats>) => { state.graphStats = action.payload; }, + + setSchemaLoading: (state, action: PayloadAction<boolean>) => { + state.loading = action.payload; + if (!action.payload) state.graph = new SchemaGraphology().export(); + }, }, }); export const { @@ -88,8 +95,10 @@ export const { clearSchema, setSchemaAttributeDimensions, setSchemaAttributeInformation, + setSchemaLoading, } = schemaSlice.actions; +export const schema = (state: RootState) => state.schema; export const schemaSettingsState = (state: RootState) => state.schema.settings; export const schemaInferenceState = (state: RootState) => state.schema.graphInference; export const schemaStatsState = (state: RootState) => state.schema.graphStats; diff --git a/libs/shared/lib/management/Explorations.tsx b/libs/shared/lib/management/Explorations.tsx new file mode 100644 index 000000000..deadcebd0 --- /dev/null +++ b/libs/shared/lib/management/Explorations.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Button } from '../components'; + +type Props = { + changeActive: (val: 'add' | 'update') => void; +}; + +export function Explorations({ changeActive }: Props) { + return ( + <div> + <div className="flex gap-x-4 mb-4"> + <Button + label="Project" + variantType="primary" + iconComponent="icon-[ic--baseline-add]" + iconPosition="leading" + onClick={() => changeActive('add')} + /> + <Button + label="Learn how to use" + variant="ghost" + iconComponent="icon-[mdi--play]" + iconPosition="leading" + onClick={() => window.open('https://graphpolaris.com/', '_blank')} + /> + </div> + <table className="w-full"> + <thead> + <tr> + <th scope="col" className="text-left"> + Name + </th> + <th scope="col" className="text-left"> + Last modified + </th> + <th scope="col" className="text-left"> + Created + </th> + <th scope="col"></th> + </tr> + </thead> + </table> + </div> + ); +} diff --git a/libs/shared/lib/management/ManagementDialog.tsx b/libs/shared/lib/management/ManagementDialog.tsx new file mode 100644 index 000000000..c0f57f042 --- /dev/null +++ b/libs/shared/lib/management/ManagementDialog.tsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import { Settings } from './Settings'; +import { Overview } from './Overview'; +import { Members } from './Members'; +import { UpsertDatabase } from './database'; +import { Button, Dialog, DialogContent, SaveStateI, useAppDispatch, useAuthCache, useSessionCache } from '..'; +import { addInfo } from '../data-access/store/configSlice'; + +type Props = { + open: boolean; + onClose: () => void; + current: ManagementViews; + setCurrent: (val: ManagementViews) => void; +}; + +export type ManagementViews = 'overview' | 'settings' | 'members' | 'add' | 'update'; + +export function ManagementDialog(props: Props) { + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const authCache = useAuthCache(); + const [selectedSaveState, setSelectedSaveState] = useState<SaveStateI | null>(null); + + const dialogMessage = (() => { + switch (props.current) { + case 'add': + return 'Add Database'; + case 'update': + return 'Updating Database Connection'; + case 'members': + return 'Manage Members'; + default: + return ''; + } + })(); + + const handleClose = () => { + props.onClose(); + props.setCurrent('overview'); + }; + + 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 clipboard functionality + dispatch(addInfo('Link copied to clipboard')); + }; + + return ( + <Dialog + open={props.open} + onOpenChange={(ret) => { + if (!ret) handleClose(); + }} + > + <DialogContent className="border bg-light w-8/12 flex-grow mx-auto px-0 py-0"> + {props.current === 'overview' ? ( + <Overview onViewChange={props.setCurrent} setSelectedSaveState={setSelectedSaveState} onClose={() => handleClose()} /> + ) : ( + <div> + <div className="flex justify-between items-center border-b p-4"> + <span className="text-secondary font-semibold">{dialogMessage}</span> + <Button iconComponent="icon-[ic--outline-close]" variant="ghost" onClick={() => handleClose()} /> + </div> + {props.current === 'settings' ? ( + <Settings /> + ) : props.current === 'members' ? ( + <Members + onConfirm={handleConfirmUsers} + onClickShareLink={handleClickShareLink} + onClose={() => { + handleClose(); + }} + /> + ) : ( + <UpsertDatabase + open={props.current} + saveState={props.current === 'update' ? selectedSaveState : null} + disableCancel={ + (session.saveStates && Object.keys(session.saveStates).length === 0) || + session.currentSaveState === '00000000-0000-0000-0000-000000000000' + } + onClose={() => { + handleClose(); + }} + /> + )} + </div> + )} + </DialogContent> + </Dialog> + ); +} diff --git a/libs/shared/lib/management/ManagementTrigger.tsx b/libs/shared/lib/management/ManagementTrigger.tsx new file mode 100644 index 000000000..ace73c3c3 --- /dev/null +++ b/libs/shared/lib/management/ManagementTrigger.tsx @@ -0,0 +1,88 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { addError } from '../data-access/store/configSlice'; +import { selectSaveState } from '../data-access/store/sessionSlice'; +import { clearQB } from '../data-access/store/querybuilderSlice'; +import { clearSchema } from '../data-access/store/schemaSlice'; +import { ManagementDialog, ManagementViews } from './ManagementDialog'; +import { DatabaseStatus, Icon, LoadingSpinner, nilUUID, useAppDispatch, useSchemaGraph, useSessionCache } from '..'; + +type Props = { + managementOpen: boolean; + setManagementOpen: (val: boolean) => void; + current: ManagementViews; + setCurrent: (val: ManagementViews) => void; +}; + +export function ManagementTrigger({ managementOpen, setManagementOpen, current, setCurrent }: Props) { + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const schemaGraph = useSchemaGraph(); + const [connecting, setConnecting] = useState<boolean>(false); + + useEffect(() => { + setConnecting(false); + }, [schemaGraph]); + + useEffect(() => { + let timeoutId: ReturnType<typeof setTimeout>; + if (connecting) { + timeoutId = setTimeout(() => { + dispatch(addError("Couldn't establish connection")); + setConnecting(false); + dispatch(selectSaveState(undefined)); + dispatch(clearQB()); + dispatch(clearSchema()); + }, 10000); + } + + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, [connecting]); + + return ( + <div> + <ManagementDialog + open={managementOpen} + onClose={() => setManagementOpen(!managementOpen)} + current={current} + setCurrent={setCurrent} + /> + <div + className="flex items-center cursor-pointer border rounded hover:bg-secondary-100 transition-colors duration-300 py-1 px-2" + onClick={() => setManagementOpen(true)} + > + {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? ( + <> + <LoadingSpinner /> + <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> + </> + ) : session.currentSaveState && session.currentSaveState in session.saveStates && session.currentSaveState !== nilUUID ? ( + <div className="flex"> + <Icon component="icon-[mdi--database-outline]" size={20} className="self-center text-secondary-700" /> + <span className="relative"> + <span + className={`absolute bottom-0.5 right-3 h-2 w-2 border border-light rounded-full ${session.testedSaveState[session.currentSaveState] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'}`} + /> + </span> + <p className="ml-1 truncate">{session.saveStates[session.currentSaveState].name}</p> + </div> + ) : session.saveStates === undefined ? ( + <> + <LoadingSpinner /> + <p className="ml-2">Retrieving databases</p> + </> + ) : Object.keys(session.saveStates).length === 0 || session.currentSaveState === nilUUID ? ( + <> + <p className="ml-2">Add your first Database</p> + </> + ) : ( + <> + <div className="h-2 w-2 rounded-full bg-secondary-500" /> + <p className="ml-2">Select a database</p> + </> + )} + </div> + </div> + ); +} diff --git a/libs/shared/lib/management/Members.tsx b/libs/shared/lib/management/Members.tsx new file mode 100644 index 000000000..64122c68e --- /dev/null +++ b/libs/shared/lib/management/Members.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { Button } from '../components/buttons'; +import { useDialogContext } from '../components/layout/Dialog'; +import { Input } from '../components/inputs'; +import { useUsersPolicy } from '../data-access/store'; +import { useDispatch } from 'react-redux'; +import { Icon } from '..'; +import { setUsersPolicy, UserPolicy } from '../data-access/store/authorizationUsersSlice'; +import { TableUI } from '../components/tableUI/TableUI'; + +interface UserManagementContentProps { + onConfirm?: (users: { name: string; email: string; type: string }[]) => void; + onClickShareLink?: () => void; + onClose?: () => void; +} + +interface FieldConfig<T> { + key: keyof T; + label: string; + type: 'text' | 'dropdown'; + required?: boolean; +} + +export const Members: React.FC<UserManagementContentProps> = ({ onConfirm, onClickShareLink, onClose }) => { + const { setOpen } = useDialogContext(); + const dispatch = useDispatch(); + + // !FIXME: This should definited at high level + const optionsTypeUser = ['', 'Creator', 'Viewer']; + + 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 })); + }; + + return ( + <div className="flex flex-col p-4"> + <div className="bg-primary-50 flex items-center px-2 gap-x-2 my-2"> + <Icon component="icon-[ic--outline-info]" size={14} /> + <span className="font-light text-sm">These members have access to all projects within your company</span> + </div> + + <div className="flex flex-col items-center flex-grow"> + <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="Add row" onClick={() => {}} /> + <Button + variantType="primary" + size="md" + label="Save" + onClick={() => { + if (onConfirm) onConfirm(usersPolicy.users); + if (onClose) onClose(); + }} + /> + </div> + </div> + </div> + ); +}; diff --git a/libs/shared/lib/management/Overview.tsx b/libs/shared/lib/management/Overview.tsx new file mode 100644 index 000000000..59f6f13e3 --- /dev/null +++ b/libs/shared/lib/management/Overview.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import { SaveStateI, useAppDispatch, useSessionCache } from '..'; +import { ManagementViews } from './ManagementDialog'; +import { Tabs, Tab } from '../components/tabs'; +import { Workspace } from './Workspace'; +import { Members } from './Members'; + +type Props = { + onClose: () => void; + onViewChange: (val: ManagementViews) => void; + setSelectedSaveState: (val: SaveStateI) => void; +}; + +export function Overview({ onClose, onViewChange, setSelectedSaveState }: Props) { + const [activeTab, setActiveTab] = useState<'workspace' | 'company' | 'settings' | 'members' | 'admin'>('workspace'); + + return ( + <div className="flex flex-col h-[calc(100vh-20rem)] max-w-[calc(100vw-20rem)] overflow-y-auto overflow-x-hidden p-8 gap-4"> + <h1 className="text-4xl font-semibold">{/* <img className="w-52" src="" /> */}Company name</h1> + <Tabs tabType="simple"> + <Tab text="Workspace" activeTab={activeTab === 'workspace'} onClick={() => setActiveTab('workspace')}></Tab> + <Tab text="Company" activeTab={activeTab === 'company'} onClick={() => setActiveTab('company')}></Tab> + {/* TODO investigate how to best integrate <Tab text="Members" activeTab={activeTab === 'members'} onClick={() => setActiveTab('members')}></Tab> */} + <Tab text="Admin Only" activeTab={activeTab === 'admin'} onClick={() => setActiveTab('admin')} variant="outline"></Tab> + </Tabs> + + <div> + {activeTab === 'workspace' && ( + <Workspace onClose={onClose} onViewChange={onViewChange} setSelectedSaveState={setSelectedSaveState} /> + )} + {/* {activeTab === 'members' && <Members />} */} + </div> + </div> + ); +} diff --git a/libs/shared/lib/management/Settings.tsx b/libs/shared/lib/management/Settings.tsx new file mode 100644 index 000000000..02987b9e6 --- /dev/null +++ b/libs/shared/lib/management/Settings.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +export function Settings() { + return ( + <div className="flex items-start w-full h-full p-6"> + <div className="w-1/3 flex flex-col items-center"> + <img src="" /> + <span className="text-primary font-semibold">Change logo</span> + </div> + <div className="w-2/3 flex flex-col gap-y-4"> + <div className="flex flex-col"> + <span className="font-bold">Company name</span> + <div className="flex gap-x-4"> + <span>Stichting VbV</span> + <span className="text-primary font-semibold">Change name</span> + </div> + </div> + <div className="flex flex-col"> + <span className="font-bold">Plan and Billing</span> + <div className="flex gap-x-4"> + <span>Standard</span> + <span className="text-primary font-semibold">Go to billing</span> + </div> + </div> + </div> + </div> + ); +} diff --git a/libs/shared/lib/management/Workspace.tsx b/libs/shared/lib/management/Workspace.tsx new file mode 100644 index 000000000..38645964d --- /dev/null +++ b/libs/shared/lib/management/Workspace.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { Button, SaveStateI, useAppDispatch, useSessionCache } from '..'; +import { Databases } from './database'; +import { ManagementViews } from './ManagementDialog'; +import { Tabs, Tab } from '../components/tabs'; + +type Props = { + onClose: () => void; + onViewChange: (val: ManagementViews) => void; + setSelectedSaveState: (val: SaveStateI) => void; +}; + +export function Workspace({ onClose, onViewChange, setSelectedSaveState }: Props) { + const session = useSessionCache(); + const [activeTab, setActiveTab] = useState('databases'); + + return ( + <div> + <Tabs tabType="rounded"> + {/* TODO <Tab text="Projects" activeTab={activeTab === 'projects'} onClick={() => setActiveTab('projects')}></Tab> */} + <Tab text="Databases" activeTab={activeTab === 'databases'} onClick={() => setActiveTab('databases')}></Tab> + {/* TODO <Tab text="Explorations" activeTab={activeTab === 'explorations'} onClick={() => setActiveTab('explorations')}></Tab> */} + </Tabs> + + <div> + {/* TODO {activeTab === 'projects' && 'Not Implemented Yet'} */} + {activeTab === 'databases' && ( + <Databases + saveStates={session.saveStates} + changeActive={(val) => onViewChange(val)} + setSelectedSaveState={setSelectedSaveState} + onClose={onClose} + /> + )} + {/* TODO {activeTab === 'explorations' && <Explorations changeActive={(val: 'add' | 'update') => onViewChange(val)} />} */} + </div> + </div> + ); +} diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx b/libs/shared/lib/management/database/DatabaseForm.tsx similarity index 94% rename from apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx rename to libs/shared/lib/management/database/DatabaseForm.tsx index 942d65c94..1f49601f3 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx +++ b/libs/shared/lib/management/database/DatabaseForm.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react'; -import { DatabaseType, SaveStateI, databaseNameMapping, databaseProtocolMapping, nilUUID } from '@graphpolaris/shared/lib/data-access'; -import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { useImmer } from 'use-immer'; -import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import { initialState as qbInitialState } from '../../data-access/store/querybuilderSlice'; +import { databaseNameMapping, databaseProtocolMapping, DatabaseType, Input, nilUUID, SaveStateI } from '../..'; export const INITIAL_SAVE_STATE: SaveStateI = { id: nilUUID, diff --git a/libs/shared/lib/management/database/Databases.tsx b/libs/shared/lib/management/database/Databases.tsx new file mode 100644 index 000000000..a3c327212 --- /dev/null +++ b/libs/shared/lib/management/database/Databases.tsx @@ -0,0 +1,203 @@ +import React, { useMemo, useState } from 'react'; +import { + Button, + DropdownItem, + Icon, + nilUUID, + SaveStateI, + useAppDispatch, + useAuthCache, + useAuthentication, + useSessionCache, + wsDeleteState, +} from '../..'; +import { deleteSaveState, selectSaveState } from '../../data-access/store/sessionSlice'; +import { clearQB } from '../../data-access/store/querybuilderSlice'; +import { clearSchema } from '../../data-access/store/schemaSlice'; +import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; +import { useHandleDatabase } from './useHandleDatabase'; + +type Props = { + onClose: () => void; + saveStates: { [id: string]: SaveStateI }; + changeActive: (val: 'add' | 'update') => void; + setSelectedSaveState: (val: SaveStateI) => void; +}; + +type DatabaseTableHeaderTypes = 'name' | 'protocol' | 'url'; + +const DataTableHeader = ({ + name, + orderBy, + setOrderBy, +}: { + name: DatabaseTableHeaderTypes; + orderBy: [DatabaseTableHeaderTypes, 'asc' | 'desc']; + setOrderBy: (val: [DatabaseTableHeaderTypes, 'asc' | 'desc']) => void; +}) => { + return ( + <th + scope="col" + className="text-left hover:underline cursor-pointer" + onClick={() => setOrderBy([name, orderBy[1] === 'desc' && orderBy[0] === name ? 'asc' : 'desc'])} + > + <span className="flex items-center gap-0"> + {name} + {orderBy[0] !== name && <div className="min-w-6 min-h-4 " />} + {orderBy[0] === name && orderBy[1] === 'asc' && <Icon component="icon-[ic--baseline-arrow-drop-up]" />} + {orderBy[0] === name && orderBy[1] === 'desc' && <Icon component="icon-[ic--baseline-arrow-drop-down]" />} + </span> + </th> + ); +}; + +export function Databases({ onClose, saveStates, changeActive, setSelectedSaveState }: Props) { + const dispatch = useAppDispatch(); + const auth = useAuthentication(); + const session = useSessionCache(); + const databaseHandler = useHandleDatabase(); + const [orderBy, setOrderBy] = useState<[DatabaseTableHeaderTypes, 'asc' | 'desc']>(['name', 'desc']); + + const orderedSaveStates = useMemo( + () => + Object.keys(saveStates).sort((a, b) => { + const dir = orderBy[1] === 'asc' ? 1 : -1; + if (orderBy[0] === 'name') { + if (saveStates[a].name.toLowerCase() <= saveStates[b].name.toLowerCase()) return dir; + else return -dir; + } else { + if (saveStates[a].db[orderBy[0]].toLowerCase() <= saveStates[b].db[orderBy[0]].toLowerCase()) return dir; + else return -dir; + } + }), + [saveStates, orderBy], + ); + + return ( + <div className="flex flex-col gap-4"> + <div className="flex gap-x-4"> + <Button + label="Database" + variantType="primary" + iconComponent="icon-[ic--baseline-add]" + iconPosition="leading" + onClick={() => changeActive('add')} + /> + <Button + label="Learn how to use" + variant="ghost" + iconComponent="icon-[mdi--play]" + iconPosition="leading" + onClick={() => window.open('https://graphpolaris.com/', '_blank')} + /> + </div> + {orderedSaveStates.length > 0 && ( + <table className="w-full"> + <thead> + <tr> + <DataTableHeader name="name" orderBy={orderBy} setOrderBy={setOrderBy} /> + <DataTableHeader name="protocol" orderBy={orderBy} setOrderBy={setOrderBy} /> + <DataTableHeader name="url" orderBy={orderBy} setOrderBy={setOrderBy} /> + </tr> + </thead> + <tbody className="divide-y divide-white overflow-auto table-fixed"> + {orderedSaveStates.map((key) => ( + <tr key={key}> + <td> + <Button + variant={session.currentSaveState === key ? 'outline' : 'ghost'} + onClick={() => { + if (key !== session.currentSaveState) { + dispatch(selectSaveState(key)); + dispatch(clearQB()); + dispatch(clearSchema()); + onClose(); + } + }} + > + {saveStates[key].name} + </Button> + </td> + <td className="text-left"> + <span className="font-light">{saveStates[key].db.protocol}</span> + </td> + <td className="text-left"> + <span className="font-light">{saveStates[key].db.url}</span> + </td> + <td className="text-right flex justify-end"> + <Button + iconComponent="icon-[mdi--trash-outline]" + variant="ghost" + onClick={() => { + if (session.currentSaveState === key) { + dispatch(clearQB()); + dispatch(clearSchema()); + } + wsDeleteState(key); + dispatch(deleteSaveState(key)); + }} + /> + <Popover> + <PopoverTrigger> + <Button iconComponent="icon-[mi--options-vertical]" variant="ghost" /> + </PopoverTrigger> + <PopoverContent className="w-56 z-50 bg-light rounded-sm border-[1px] outline-none"> + <DropdownItem + value="Open" + onClick={() => { + if (key !== session.currentSaveState) { + dispatch(selectSaveState(key)); + dispatch(clearQB()); + dispatch(clearSchema()); + } + onClose(); + }} + /> + <DropdownItem + value="Update" + onClick={() => { + changeActive('update'); + setSelectedSaveState(saveStates[key]); + }} + /> + <DropdownItem + value="Clone" + onClick={() => { + databaseHandler.submitDatabaseChange( + { ...saveStates[key], name: saveStates[key].name + ' (copy)', id: nilUUID }, + 'add', + true, + () => {}, + ); + setSelectedSaveState(saveStates[key]); + }} + /> + <DropdownItem + value="Share" + onClick={() => { + auth.newShareRoom(); + }} + /> + <DropdownItem + value="Delete" + onClick={() => { + if (session.currentSaveState === key) { + dispatch(clearQB()); + dispatch(clearSchema()); + } + wsDeleteState(key); + dispatch(deleteSaveState(key)); + }} + className="text-danger" + /> + </PopoverContent> + </Popover> + </td> + </tr> + ))} + </tbody> + </table> + )} + </div> + ); +} diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx b/libs/shared/lib/management/database/MockSaveStates.tsx similarity index 93% rename from apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx rename to libs/shared/lib/management/database/MockSaveStates.tsx index 25ccd998c..7fdaa0a19 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx +++ b/libs/shared/lib/management/database/MockSaveStates.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { DatabaseType, SaveStateI, nilUUID } from '@graphpolaris/shared/lib/data-access/broker'; -import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import { DatabaseType, SaveStateI, nilUUID } from '../../data-access/broker'; +import { initialState as qbInitialState } from '../../data-access/store/querybuilderSlice'; export type SaveStateSampleI = SaveStateI & { subtitle: string; @@ -132,11 +132,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ export const SampleDatabaseSelector = (props: { onClick: (data: SaveStateI) => void }) => { return ( - <div className="grid grid-cols-2 lg:grid-cols-3 gap-2"> + <div className="flex flex-wrap gap-2"> {sampleSaveStates.map((sample) => ( <div key={sample.name} - className="card hover:bg-secondary-100 cursor-pointer mb-2 border w-[15rem]" + className="card hover:bg-secondary-100 cursor-pointer mb-2 border w-[15rem] flex-grow " onClick={() => props.onClick(sample as SaveStateI)} > <div className="card-body"> diff --git a/libs/shared/lib/management/database/UpsertDatabase.tsx b/libs/shared/lib/management/database/UpsertDatabase.tsx new file mode 100644 index 000000000..5c6fa1513 --- /dev/null +++ b/libs/shared/lib/management/database/UpsertDatabase.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { SaveStateI, useAuthCache, nilUUID } from '../../data-access'; +import { Button } from '../../components/buttons'; +import { useImmer } from 'use-immer'; +import { DatabaseForm, INITIAL_SAVE_STATE } from './DatabaseForm'; +import { SampleDatabaseSelector } from './MockSaveStates'; +import { Icon } from '../..'; +import { useHandleDatabase } from './useHandleDatabase'; + +export const UpsertDatabase = (props: { + onClose(): void; + open: 'add' | 'update'; + saveState: SaveStateI | null; + disableCancel?: boolean; +}) => { + const databaseHandler = useHandleDatabase(); + const ref = useRef<HTMLDialogElement>(null); + const authCache = useAuthCache(); + const [formData, setFormData] = useImmer( + props.saveState && props.open === 'update' + ? props.saveState + : { ...INITIAL_SAVE_STATE, user_id: authCache.authentication?.userID || '' }, + ); + const [hasError, setHasError] = useState(false); + const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false); + const formTitle = props.open === 'add' ? 'Add' : 'Update'; + + useEffect(() => { + if (props.saveState && props.open === 'update') { + setFormData(props.saveState); + setSampleDataPanel(null); + } else { + setSampleDataPanel(false); + } + }, [props.saveState]); + + async function handleSubmit(saveStateData?: SaveStateI, forceAdd: boolean = false): Promise<void> { + if (!saveStateData) saveStateData = formData; + databaseHandler.submitDatabaseChange(saveStateData, props.open, forceAdd, () => { + closeDialog(); + }); + } + + function closeDialog(): void { + setFormData({ ...INITIAL_SAVE_STATE, user_id: authCache.authentication?.userID || '' }); + ref.current?.close(); + props.onClose(); + } + + return ( + <div className="lg:min-w-[50rem] p-6"> + <> + {sampleDataPanel === true ? ( + <SampleDatabaseSelector + onClick={(data) => { + setHasError(false); + handleSubmit({ ...data, user_id: authCache.authentication?.userID || '' }); + }} + /> + ) : ( + <DatabaseForm + data={formData} + onChange={(data: SaveStateI, error: boolean) => { + setFormData({ ...data, id: formData.id }); + setHasError(error); + }} + /> + )} + + {!(databaseHandler.connectionStatus.status === null) && ( + <div className={`flex flex-col justify-center items-center`}> + <div className="flex justify-center items-center"> + {databaseHandler.connectionStatus.verified === false && ( + <Icon component="icon-[ic--baseline-error-outline]" className="text-secondary-400" /> + )} + <p className="font-light text-sm text-secondary-400 ">{databaseHandler.connectionStatus.status}</p> + </div> + {databaseHandler.connectionStatus.verified === null && <progress className="progress w-56"></progress>} + </div> + )} + + <div + className={`pt-4 flex flex-row gap-3 card-actions w-full justify-between items-center ${sampleDataPanel === true && 'hidden'}`} + > + <div className="flex justify-between items-center"> + <Button + variantType="primary" + className="flex-grow" + label={ + databaseHandler.connectionStatus.updating + ? formTitle === 'Add' + ? formTitle + 'ing...' + : formTitle.slice(0, -1) + 'ing...' + : formTitle + } + onClick={(event) => { + event.preventDefault(); + handleSubmit(); + }} + disabled={databaseHandler.connectionStatus.updating || hasError} + /> + {props.open === 'update' && ( + <Button + variantType="secondary" + className="flex-grow" + label={'Clone'} + onClick={(event) => { + handleSubmit({ ...formData, name: formData.name + ' (copy)', id: nilUUID }, true); + }} + disabled={databaseHandler.connectionStatus.updating || hasError} + /> + )} + </div> + <div className="flex justify-end align-center mx-2"> + <div> + {sampleDataPanel === true ? ( + <Button variant="outline" label="Go back" onClick={() => setSampleDataPanel(false)} /> + ) : sampleDataPanel === false ? ( + <> + <h1 className="font-light text-xs">No data?</h1> + <p className="font-light text-sm cursor-pointer underline" onClick={() => setSampleDataPanel(true)}> + Try sample data + </p> + </> + ) : ( + '' + )} + </div> + </div> + </div> + </> + </div> + ); +}; diff --git a/libs/shared/lib/management/database/index.ts b/libs/shared/lib/management/database/index.ts new file mode 100644 index 000000000..f723a67e6 --- /dev/null +++ b/libs/shared/lib/management/database/index.ts @@ -0,0 +1,2 @@ +export * from './Databases'; +export * from './UpsertDatabase'; diff --git a/libs/shared/lib/management/database/useHandleDatabase.ts b/libs/shared/lib/management/database/useHandleDatabase.ts new file mode 100644 index 000000000..2f03281c9 --- /dev/null +++ b/libs/shared/lib/management/database/useHandleDatabase.ts @@ -0,0 +1,91 @@ +import { + SaveStateI, + wsTestDatabaseConnection, + wsCreateState, + wsUpdateState, + useAuthCache, + useAppDispatch, +} from '../../data-access'; +import { useState } from 'react'; +import { addSaveState, selectSaveState, testedSaveState } from '../../data-access/store/sessionSlice'; +import { setSchemaLoading } from '../../data-access/store/schemaSlice'; + +export type ConnectionStatus = { + updating: boolean; + status: null | string; + verified: boolean | null; +}; + +export const useHandleDatabase = () => { + const dispatch = useAppDispatch(); + const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>({ + updating: false, + status: null, + verified: null, + }); + + const authCache = useAuthCache(); + + async function submitDatabaseChange( + saveStateData: SaveStateI, + type: 'add' | 'update', + forceAdd: boolean = false, + concludedCallback: () => void, + ): Promise<void> { + setConnectionStatus(() => ({ + updating: true, + status: 'Testing database connection', + verified: null, + })); + + wsTestDatabaseConnection(saveStateData.db, (data) => { + if (!saveStateData) { + console.error('formData is null'); + return; + } + if (saveStateData.user_id !== authCache.authentication?.userID && authCache.authentication?.userID) { + console.error('user_id is not equal to auth.userID'); + saveStateData.user_id = authCache.authentication.userID; + } + if (data && data.status === 'success') { + setConnectionStatus((prevState) => ({ + updating: false, + status: 'Database connection verified', + verified: true, + })); + dispatch(setSchemaLoading(true)); + dispatch(selectSaveState(undefined)); + if (type === 'add' || forceAdd) { + wsCreateState(saveStateData, (newSaveState) => { + dispatch(addSaveState(newSaveState)); + dispatch(testedSaveState(newSaveState.id)); + setConnectionStatus({ + updating: false, + status: null, + verified: null, + }); + concludedCallback(); + }); + } else { + dispatch(testedSaveState(data.saveStateID)); + wsUpdateState(saveStateData, (updatedSaveState) => { + dispatch(addSaveState(updatedSaveState)); + setConnectionStatus({ + updating: false, + status: null, + verified: null, + }); + concludedCallback(); + }); + } + } else { + setConnectionStatus((prevState) => ({ + updating: false, + status: 'Database connection test failed', + verified: false, + })); + } + }); + } + return { submitDatabaseChange, connectionStatus }; +}; diff --git a/libs/shared/lib/management/index.ts b/libs/shared/lib/management/index.ts new file mode 100644 index 000000000..90a09e7d3 --- /dev/null +++ b/libs/shared/lib/management/index.ts @@ -0,0 +1,2 @@ +export * from './ManagementTrigger'; +export * from './ManagementDialog'; diff --git a/libs/shared/lib/schema/panel/Schema.tsx b/libs/shared/lib/schema/panel/Schema.tsx index ef704b8f9..fce8bad96 100644 --- a/libs/shared/lib/schema/panel/Schema.tsx +++ b/libs/shared/lib/schema/panel/Schema.tsx @@ -3,11 +3,11 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import ReactFlow, { Edge, MiniMap, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'; import 'reactflow/dist/style.css'; -import { Panel } from '../../components'; +import { Icon, Panel } from '../../components'; import { Button } from '../../components/buttons'; import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/tooltip/Tooltip'; -import { useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access'; +import { useSchema, useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access'; import { resultSetFocus } from '../../data-access/store/interactionSlice'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; @@ -81,8 +81,8 @@ export const Schema = (props: Props) => { const reactFlowRef = useRef<HTMLDivElement>(null); // In case the schema is updated - const schemaGraph = useSchemaGraph(); - const schemaGraphology = useMemo(() => toSchemaGraphology(schemaGraph), [schemaGraph]); + const schema = useSchema(); + const schemaGraphology = useMemo(() => toSchemaGraphology(schema.graph), [schema.graph]); const layout = useRef<AlgorithmToLayoutProvider<AllLayoutAlgorithms>>(); const [viewSelected, setViewState] = useState<SchemaViewState>(SchemaViewState.SchemaListS); @@ -226,7 +226,7 @@ export const Schema = (props: Props) => { } else { layoutList(); } - }, [schemaGraph, settings]); + }, [schema.graph, settings]); useEffect(() => { setNodes((nds) => @@ -303,7 +303,7 @@ export const Schema = (props: Props) => { iconComponent="icon-[ic--baseline-content-copy]" onClick={() => { // Copy the schema to the clipboard - navigator.clipboard.writeText(JSON.stringify(schemaGraph, null, 2)); + navigator.clipboard.writeText(JSON.stringify(schema.graph, null, 2)); }} /> </TooltipTrigger> @@ -352,7 +352,11 @@ export const Schema = (props: Props) => { } > <div className="schema-panel w-full h-full flex flex-col justify-between" ref={reactFlowRef}> - {nodes.length === 0 ? ( + {schema.loading ? ( + <div className="h-full flex flex-col items-center justify-center"> + <Icon component="icon-[mingcute--loading-line]" size={56} className="w-15 h-15 animate-spin " /> + </div> + ) : nodes.length === 0 ? ( <p className="m-3 text-xl font-bold">No Elements</p> ) : ( <ReactFlowProvider> diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx index e705499e9..16e539086 100644 --- a/libs/shared/lib/vis/components/VisualizationPanel.tsx +++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx @@ -31,7 +31,7 @@ export const Visualizations: Record<string, PromiseFunc> = { }), ...(isVisualizationReleased('MapVis') && { MapVis: () => import('../visualizations/mapvis/mapvis') }), ...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/Vis0D/Vis0D') }), - ...(isVisualizationReleased('Vis1D') && { Vis0D: () => import('../visualizations/vis1D/Vis1D') }), + ...(isVisualizationReleased('Vis1D') && { Vis1D: () => import('../visualizations/vis1D/Vis1D') }), }; export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations); diff --git a/libs/shared/lib/vis/components/VisualizationTabBar.tsx b/libs/shared/lib/vis/components/VisualizationTabBar.tsx index 9d229a1a1..b186aebf8 100644 --- a/libs/shared/lib/vis/components/VisualizationTabBar.tsx +++ b/libs/shared/lib/vis/components/VisualizationTabBar.tsx @@ -17,11 +17,11 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor e.dataTransfer.setData('text/plain', i.toString()); }; - const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { + const handleDragOver = (e: React.DragEvent<HTMLButtonElement>) => { e.preventDefault(); }; - const handleDrop = (e: React.DragEvent<HTMLDivElement>, i: number) => { + const handleDrop = (e: React.DragEvent<HTMLButtonElement>, i: number) => { e.preventDefault(); const draggedVisIndex = e.dataTransfer.getData('text/plain'); dispatch(reorderVisState({ id: Number(draggedVisIndex), newPosition: i })); -- GitLab