diff --git a/README.md b/README.md index f7b3c2b9..798025cd 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ const main = () => { - `visualizeAttributes` (optional)(`string[]`): List of attribute names (strings) which should be visualized. Defaults to the first 3 if no value is provided. If an empty list is provided, displays no attributes. - `visualizeUpsetAttributes` (optional)(`boolean`): Whether or not to visualize UpSet generated attributes (`degree` and `deviation`). Defaults to `false`. - `allowAttributeRemoval` (optional)(`boolean`): Whether or not to allow the user to remove attribute columns. This should be enabled only if there is an option within the parent application which allows for attributes to be added after removal. Default attribute removal behavior in UpSet 2.0 is done via context menu on attribute headers. Defaults to `false`. +- `canEditPlotInformation` (optional)(`boolean`): Whether or not the user can edit the plot information in the text descriptions sidebar. - `hideSettings` (optional)(`boolean`): Hide the aggregations/filter settings sidebar. - `parentHasHeight` (optional)(`boolean`): Indicates if the parent component has a fixed height. If this is set to `false`, the plot will occupy the full viewport height. When set to `true`, the plot will fit entirely within the parent component. Defaults to `false`. - `extProvenance` (optional): External provenance actions and [TrrackJS](https://github.com/Trrack/trrackjs) object for provenance history tracking and actions. This should only be used if your tool is using TrrackJS and the Trrack object you provide has all the actions used by UpSet 2.0. Provenance is still tracked if nothing is provided. See [App.tsx](https://github.com/visdesignlab/upset2/blob/main/packages/app/src/App.tsx) to see how UpSet 2.0 and Multinet use an external Trrack object. Note that [initializeProvenanceTracking](https://github.com/visdesignlab/upset2/blob/main/packages/upset/src/provenance/index.ts#L300) and [getActions](https://github.com/visdesignlab/upset2/blob/main/packages/upset/src/provenance/index.ts#L322) are used to ensure that the provided provenance object is compatible. The provided provenance object must have a type compatible with the [extProvenance](https://vdl.sci.utah.edu/upset2/interfaces/_visdesignlab_upset2_react.UpsetProps.html#extProvenance) UpSet 2.0 prop type. diff --git a/e2e-tests/common.ts b/e2e-tests/common.ts index fb4ff738..0d456a1c 100644 --- a/e2e-tests/common.ts +++ b/e2e-tests/common.ts @@ -34,6 +34,9 @@ export async function beforeTest({ page }: {page: Page}) { await route.fulfill({ json }); } else if (url.includes('workspaces/Upset%20Examples/sessions/table/193/')) { await route.fulfill({ status: 200 }); + } else if (url.includes('workspaces/Upset%20Examples/permissions/me')) { + // User has owner permissions, this will allow plot information editing + await route.fulfill({ status: 200, json: { permission_label: 'owner' } }); } else { await route.continue(); } diff --git a/packages/app/src/components/Body.tsx b/packages/app/src/components/Body.tsx index 548b26d1..11972041 100644 --- a/packages/app/src/components/Body.tsx +++ b/packages/app/src/components/Body.tsx @@ -5,7 +5,7 @@ import { encodedDataAtom } from '../atoms/dataAtom'; import { doesHaveSavedQueryParam, queryParamAtom, saveQueryParam } from '../atoms/queryParamAtom'; import { ErrorModal } from './ErrorModal'; import { ProvenanceContext } from '../App'; -import { useContext, useEffect } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { provenanceVisAtom } from '../atoms/provenanceVisAtom'; import { elementSidebarAtom } from '../atoms/elementSidebarAtom'; import { altTextSidebarAtom } from '../atoms/altTextSidebarAtom'; @@ -13,6 +13,7 @@ import { loadingAtom } from '../atoms/loadingAtom'; import { Backdrop, CircularProgress } from '@mui/material'; import { updateMultinetSession } from '../api/session'; import { generateAltText } from '../api/generateAltText'; +import { api } from '../api/api'; import { rowsSelector } from '../atoms/selectors'; type Props = { @@ -51,6 +52,24 @@ export const Body = ({ data, config }: Props) => { }) }, [provObject.provenance, sessionId, workspace]); + // Check if the user has permissions to edit the plot + const [permissions, setPermissions] = useState(false); + + useEffect(() => { + const fetchPermissions = async () => { + try { + const r = await api.getCurrentUserWorkspacePermissions(workspace || ''); + // https://api.multinet.app/swagger/?format=openapi#/definitions/PermissionsReturn for possible permissions returns + setPermissions(r.permission_label === 'owner' || r.permission_label === 'maintainer'); + } catch (e) { + setPermissions(false) + return; + } + }; + + fetchPermissions(); + }, [workspace]); + /** * Generates alt text for a plot based on the current state and configuration. * If an error occurs during the generation, an error message is returned. @@ -109,6 +128,7 @@ export const Body = ({ data, config }: Props) => { ({ + key: 'canEditPlotInformationAtom', + default: false, +}); diff --git a/packages/upset/src/components/AltTextSidebar.tsx b/packages/upset/src/components/AltTextSidebar.tsx index 8e603fa0..83dca27c 100644 --- a/packages/upset/src/components/AltTextSidebar.tsx +++ b/packages/upset/src/components/AltTextSidebar.tsx @@ -24,6 +24,7 @@ import '../index.css'; import { PlotInformation } from './custom/PlotInformation'; import { UpsetActions } from '../provenance'; import { plotInformationSelector } from '../atoms/config/plotInformationAtom'; +import { canEditPlotInformationAtom } from '../atoms/config/canEditPlotInformationAtom'; /** * Props for the AltTextSidebar component. @@ -66,6 +67,7 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext); const currState = useRecoilValue(upsetConfigAtom); + const canEditPlotInformation = useRecoilValue(canEditPlotInformationAtom); const [altText, setAltText] = useState(null); const [textGenErr, setTextGenErr] = useState(false); const [textEditing, setTextEditing] = useState(false); @@ -83,6 +85,10 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { * Handler for when the save button is clicked */ const saveButtonClick: () => void = useCallback(() => { + // if the user doesn't have edit permissions, don't allow saving + // The user shouldn't be able to edit in this case, but this is a failsafe + if (!canEditPlotInformation) return; + setTextEditing(false); if (!currState.useUserAlt) actions.setUseUserAltText(true); if (currState.userAltText?.shortDescription !== userShortText @@ -93,6 +99,10 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { * Sets text editing to true and sets default user alttexts if necessary */ const enableTextEditing: () => void = useCallback(() => { + // if the user doesn't have edit permissions, don't allow editing + // The button should be hidden in this case, but this is a failsafe + if (!canEditPlotInformation) return; + setTextEditing(true); if (!currState.userAltText?.shortDescription) setUserShortText(altText?.shortDescription); if (!currState.userAltText?.longDescription) setUserLongText(altText?.longDescription); @@ -206,12 +216,15 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { setEditing={setPlotInfoEditing} /> ) : ( - + // only show "Add Plot Information" if the user has edit permissions + canEditPlotInformation ? ( + + ) : null )} {displayPlotInfo && ( <> @@ -268,21 +281,24 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { }} tabIndex={3} > - + {canEditPlotInformation && ( + // Only show the edit button if the user has edit permissions + + )} diff --git a/packages/upset/src/components/Root.tsx b/packages/upset/src/components/Root.tsx index 6ae1ad8d..23a5d719 100644 --- a/packages/upset/src/components/Root.tsx +++ b/packages/upset/src/components/Root.tsx @@ -14,6 +14,7 @@ import { dataAtom } from '../atoms/dataAtom'; import { allowAttributeRemovalAtom } from '../atoms/config/allowAttributeRemovalAtom'; import { contextMenuAtom } from '../atoms/contextMenuAtom'; import { upsetConfigAtom } from '../atoms/config/upsetConfigAtoms'; +import { canEditPlotInformationAtom } from '../atoms/config/canEditPlotInformationAtom'; import { getActions, initializeProvenanceTracking, UpsetActions, UpsetProvenance, } from '../provenance'; @@ -41,6 +42,7 @@ type Props = { config: UpsetConfig; allowAttributeRemoval?: boolean; hideSettings?: boolean; + canEditPlotInformation?: boolean; extProvenance?: { provenance: UpsetProvenance; actions: UpsetActions; @@ -61,13 +63,13 @@ type Props = { }; export const Root: FC = ({ - data, config, allowAttributeRemoval, hideSettings, extProvenance, provVis, elementSidebar, altTextSidebar, generateAltText, + data, config, allowAttributeRemoval, hideSettings, canEditPlotInformation, extProvenance, provVis, elementSidebar, altTextSidebar, generateAltText, }) => { // Get setter for recoil config atom const setState = useSetRecoilState(upsetConfigAtom); - const [sets, setSets] = useRecoilState(setsAtom); const [items, setItems] = useRecoilState(itemsAtom); + const setcanEditPlotInformation = useSetRecoilState(canEditPlotInformationAtom); const setAttributeColumns = useSetRecoilState(attributeAtom); const setAllColumns = useSetRecoilState(columnsAtom); const setData = useSetRecoilState(dataAtom); @@ -79,6 +81,12 @@ export const Root: FC = ({ setData(data); }, []); + useEffect(() => { + if (canEditPlotInformation !== undefined) { + setcanEditPlotInformation(canEditPlotInformation); + } + }, [canEditPlotInformation]); + // Initialize Provenance and pass it setter to connect const { provenance, actions } = useMemo(() => { if (extProvenance) { diff --git a/packages/upset/src/components/Upset.tsx b/packages/upset/src/components/Upset.tsx index f5f4a68c..e783ee78 100644 --- a/packages/upset/src/components/Upset.tsx +++ b/packages/upset/src/components/Upset.tsx @@ -20,6 +20,7 @@ const defaultVisibleSets = 6; * @param {boolean} [allowAttributeRemoval=false] - Whether or not to allow the user to remove attribute columns. This should be enabled only if there is an option within the parent application which allows for attributes to be added after removal. Default attribute removal behavior in UpSet 2.0 is done via context menu on attribute headers. Defaults to `false`. * @param {boolean} [hideSettings] - Hide the aggregations/filter settings sidebar. * @param {boolean} [parentHasHeight=false] - Indicates if the parent component has a fixed height. If this is set to `false`, the plot will occupy the full viewport height. When set to `true`, the plot will fit entirely within the parent component. Defaults to `false`. + * @param {boolean} [canEditPlotInformation=false] - Whether or not the user has plot information edit permissions. * @param {Object} [extProvenance] - External provenance actions and [TrrackJS](https://github.com/Trrack/trrackjs) object for provenance history tracking and actions. This should only be used if your tool is using TrrackJS and has all the actions used by UpSet 2.0. Provenance is still tracked if nothing is provided. See [App.tsx](https://github.com/visdesignlab/upset2/blob/main/packages/app/src/App.tsx) to see how UpSet 2.0 and Multinet use an external Trrack object. Note that [initializeProvenanceTracking](https://github.com/visdesignlab/upset2/blob/main/packages/upset/src/provenance/index.ts#L300) and [getActions](https://github.com/visdesignlab/upset2/blob/main/packages/upset/src/provenance/index.ts#L322) are used to ensure that the provided provenance object is compatible. * @param {SidebarProps} [provVis] - The provenance visualization sidebar options. * @param {SidebarProps} [elementSidebar] - The element sidebar options. This sidebar is used for element queries, element selection datatable, and supplimental plot generation. @@ -34,6 +35,7 @@ export const Upset: FC = ({ visualizeDatasetAttributes, visualizeUpsetAttributes = false, allowAttributeRemoval = false, + canEditPlotInformation = false, hideSettings, extProvenance, provVis, @@ -107,6 +109,7 @@ export const Upset: FC = ({ data={processData} config={combinedConfig} allowAttributeRemoval={allowAttributeRemoval} + canEditPlotInformation={canEditPlotInformation} hideSettings={hideSettings} extProvenance={extProvenance} provVis={provVis} diff --git a/packages/upset/src/components/custom/PlotInformation.tsx b/packages/upset/src/components/custom/PlotInformation.tsx index f2a23377..6762d9bc 100644 --- a/packages/upset/src/components/custom/PlotInformation.tsx +++ b/packages/upset/src/components/custom/PlotInformation.tsx @@ -12,6 +12,7 @@ import { useContext, useState, useCallback, } from 'react'; import { plotInformationSelector } from '../../atoms/config/plotInformationAtom'; +import { canEditPlotInformationAtom } from '../../atoms/config/canEditPlotInformationAtom'; import { ProvenanceContext } from '../Root'; /** @@ -85,6 +86,7 @@ export const PlotInformation = ({ */ const plotInformationState = useRecoilValue(plotInformationSelector); + const canEditPlotInformation = useRecoilValue(canEditPlotInformationAtom); const [plotInformation, setPlotInformation] = useState(plotInformationState); const { actions } = useContext(ProvenanceContext); @@ -142,21 +144,24 @@ export const PlotInformation = ({ }} >
- + {canEditPlotInformation && ( + // Hide the edit button if the user does not have permissions + + )} {plotInformation.caption}

diff --git a/packages/upset/src/types.ts b/packages/upset/src/types.ts index 60a2b55b..0424ffa3 100644 --- a/packages/upset/src/types.ts +++ b/packages/upset/src/types.ts @@ -127,6 +127,11 @@ export interface UpsetProps { */ allowAttributeRemoval?: boolean; + /** + * Whether or not the user has plot information edit permissions. + */ + canEditPlotInformation?: boolean; + /** * Hide the aggregations/filter settings sidebar. */