Skip to content

Commit

Permalink
Merge pull request #71 from ssi-dk/ahp/feat-workspaces
Browse files Browse the repository at this point in the history
Basic workspace functionality
  • Loading branch information
allanhvam authored Jun 3, 2024
2 parents d5db8fd + f8d121c commit eaaebde
Show file tree
Hide file tree
Showing 40 changed files with 2,776 additions and 59 deletions.
2 changes: 1 addition & 1 deletion app/src/app/analysis/analysis-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from "./analysis-query-configs";
import HalfHolyGrailLayout from "../../layouts/half-holy-grail";
import AnalysisSidebar from "./sidebar/analysis-sidebar";
import AnalysisViewSelector from "./view-selector/analysis-view-selector";
import { AnalysisViewSelector } from "./view-selector/analysis-view-selector";
import AnalysisSearch from "./search/analysis-search";
import { setSelection } from "./analysis-selection-configs";
import { fetchApprovalMatrix } from "./analysis-approval-configs";
Expand Down
2 changes: 2 additions & 0 deletions app/src/app/analysis/analysis-selection-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Menu, MenuList, MenuButton, Button, MenuItem } from "@chakra-ui/react";
import { useCallback } from "react";
import { clearSelection } from "./analysis-selection-configs";
import { useDispatch } from "react-redux";
import { SendToWorkspaceMenuItem } from "app/workspaces/send-to-workspace-menu-item";

type Props = {
selection: DataTableSelection<AnalysisResult>;
Expand Down Expand Up @@ -34,6 +35,7 @@ export const AnalysisSelectionMenu = (props: Props) => {
<MenuList>
<ResistanceMenuItem selection={selection} />
<NearestNeighborMenuItem selection={selection} />
<SendToWorkspaceMenuItem selection={selection} />
<MenuItem
aria-label="Clear Selection"
title="Clear Selection"
Expand Down
22 changes: 12 additions & 10 deletions app/src/app/analysis/data-table/data-table-column-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type DataTableColumnHeaderProps<T extends NotEmpty> = {
canSelectColumn: (column: string) => boolean;
onSelectColumn: (column: Column<T>) => void;
onResize: (columnIndex: number) => void;
onSort: ({ column: string, ascending: boolean }) => void;
onSort?: ({ column: string, ascending: boolean }) => void;
};

function DataTableColumnHeader<T extends NotEmpty>(
Expand Down Expand Up @@ -60,7 +60,7 @@ function DataTableColumnHeader<T extends NotEmpty>(
tabIndex={column.index}
role="columnheader"
key={column?.id}
{...(column.getHeaderProps(column.getSortByToggleProps()) as any)}
{...column.getHeaderProps(column.getSortByToggleProps())}
title={undefined} // hides the "Toggle SortBy" tooltip
onClick={noop} // Do not sort on header-click -- handled by button
onKeyDown={noop}
Expand All @@ -81,14 +81,16 @@ function DataTableColumnHeader<T extends NotEmpty>(
<span css={headerName}>{column.render("Header")}</span>
</div>
</Tooltip>
<button
type="button"
css={headerButton}
onClick={toggleSortHandler}
onKeyDown={toggleSortHandler}
>
{column.isSorted ? (column.isSortedDesc ? " ⯯" : " ⯭") : " ⬍"}
</button>
{onSort ? (
<button
type="button"
css={headerButton}
onClick={toggleSortHandler}
onKeyDown={toggleSortHandler}
>
{column.isSorted ? (column.isSortedDesc ? " ⯯" : " ⯭") : " ⬍"}
</button>
) : null}
</div>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<div
Expand Down
63 changes: 36 additions & 27 deletions app/src/app/analysis/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ export type DataTableSelection<T extends object> = Record<
type DataTableProps<T extends NotEmpty> = {
columns: Column<T>[];
setNewColumnOrder: (columnOrder: string[]) => void;
setColumnSort: (columnSort: { column: string; ascending: boolean }) => void;
setColumnSort?: (columnSort: { column: string; ascending: boolean }) => void;
data: T[];
primaryKey: keyof T;
canSelectColumn: (columnName: string) => boolean;
canApproveColumn: (columnName: string) => boolean;
isJudgedCell: (rowId: string, columnName: string) => boolean;
getDependentColumns: (columnName: keyof T) => Array<keyof T>;
selectionClassName: string;
selectionClassName?: string;
approvableColumns: string[];
onSelect: (sel: DataTableSelection<T>) => void;
onSelect?: (sel: DataTableSelection<T>) => void;
selection: DataTableSelection<T>;
onDetailsClick: (isolateId: string, row: Row<T>) => void;
onDetailsClick?: (isolateId: string, row: Row<T>) => void;
view: UserDefinedViewInternal;
getCellStyle: (
rowId: string,
Expand All @@ -69,7 +69,9 @@ type DataTableProps<T extends NotEmpty> = {
renderCellControl: (
rowId: string,
columnId: string,
value: string
value: string,
columnIndex: number,
original: T
) => JSX.Element;
};

Expand Down Expand Up @@ -106,9 +108,12 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
selection,
} = props;

const isInSelection = React.useCallback((rowId, columnId) => {
return selection[rowId]?.cells?.[columnId];
}, [selection]);
const isInSelection = React.useCallback(
(rowId, columnId) => {
return selection[rowId]?.cells?.[columnId];
},
[selection]
);

const [lastView, setLastView] = useState(view);

Expand Down Expand Up @@ -143,8 +148,7 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
};
getDependentColumns(columnId).forEach((v) => {
incSel[rowId].cells[v] = !(
selection[rowId]?.cells &&
selection[rowId]?.cells[columnId]
selection[rowId]?.cells && selection[rowId]?.cells[columnId]
);
});
onSelect(incSel);
Expand Down Expand Up @@ -377,9 +381,7 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
.map((r) => r.original[primaryKey])
.forEach((r: string) => {
if (incSel[r]) {
incSel[r].cells[c] = !(
selection[r] && selection[r].cells[c]
);
incSel[r].cells[c] = !(selection[r] && selection[r].cells[c]);
}
});
});
Expand Down Expand Up @@ -458,7 +460,7 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
: cellStyle;

if (isInSelection(rowId, columnId)) {
className = `${className} ${selectionClassName}`;
className = `${className} ${selectionClassName ?? ""}`;
}

return (
Expand All @@ -473,24 +475,30 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
<Flex minWidth="full" minHeight="full">
{columnIndex === 0 && (
<React.Fragment>
<SelectionCheckBox
onClick={rowClickHandler(rows[rowIndex - 1])}
{...calcRowSelectionState(rows[rowIndex - 1])}
/>
<IconButton
size="1em"
variant="unstyled"
onClick={() => onDetailsClick(rowId, row)}
aria-label="Search database"
icon={<ExternalLinkIcon marginTop="-0.5em" />}
ml="1"
/>
{onSelect ? (
<SelectionCheckBox
onClick={rowClickHandler(rows[rowIndex - 1])}
{...calcRowSelectionState(rows[rowIndex - 1])}
/>
) : null}
{onDetailsClick ? (
<IconButton
size="1em"
variant="unstyled"
onClick={() => onDetailsClick(rowId, row)}
aria-label="Search database"
icon={<ExternalLinkIcon marginTop="-0.5em" />}
ml="1"
/>
) : null}
</React.Fragment>
)}
{renderCellControl(
rowId,
columnId,
rows[rowIndex - 1].original[columnId]
rows[rowIndex - 1].original[columnId],
columnIndex,
rows[rowIndex - 1].original
)}
</Flex>
</div>
Expand All @@ -514,6 +522,7 @@ function DataTable<T extends NotEmpty>(props: DataTableProps<T>) {
onDetailsClick,
renderCellControl,
cellClickHandler,
onSelect,
]
);

Expand Down
61 changes: 41 additions & 20 deletions app/src/app/analysis/view-selector/analysis-view-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,16 @@ import {
import { defaultViews, setView } from "./analysis-view-selection-config";
import { spyDataTable } from "../data-table/table-spy";

const AnalysisViewSelector = () => {
type Props = {
manageViews?: boolean;
};

export const AnalysisViewSelector = (props: Props) => {
const { manageViews } = props;
const { t } = useTranslation();
const addViewValue = "AddView";
const deleteViewValue = "DeleteView";

const buildOptions = React.useCallback(
(options: UserDefinedViewInternal[]) => [
{ label: `-- ${t("Save current view")}`, value: addViewValue },
{ label: `-- ${t("Delete current view")}`, value: deleteViewValue },
{
label: t("Predefined views"),
options: defaultViews.map((x) => ({ label: x.name, value: x })),
},
{
label: t("My views"),
options: options.map((x) => ({ label: x.name, value: x })),
},
],
[t]
);

const dispatch = useDispatch();
const userViews = useSelector<RootState>(
(s) => {
Expand Down Expand Up @@ -111,9 +100,43 @@ const AnalysisViewSelector = () => {
view,
]);

const selectOptions = React.useMemo(() => {
let options = new Array<
| { label: string; value: string | UserDefinedViewInternal }
| {
label: string;
options: Array<{
label: string;
value: string | UserDefinedViewInternal;
}>;
}
>();

if (manageViews !== false) {
options = options.concat([
{ label: `-- ${t("Save current view")}`, value: addViewValue },
{ label: `-- ${t("Delete current view")}`, value: deleteViewValue },
]);
}

options.push({
label: t("Predefined views"),
options: defaultViews.map((x) => ({ label: x.name, value: x })),
});

if (isFinished) {
options.push({
label: t("My views"),
options: userViews.map((x) => ({ label: x.name, value: x })),
});
}

return options;
}, [t, isFinished, userViews, manageViews]);

return (
<Select
options={isFinished ? buildOptions(userViews) : ([] as any[])}
options={selectOptions}
defaultValue={value}
value={value}
theme={selectTheme}
Expand All @@ -122,5 +145,3 @@ const AnalysisViewSelector = () => {
/>
);
};

export default AnalysisViewSelector;
21 changes: 20 additions & 1 deletion app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import AnalysisPage from "./analysis/analysis-page";
import ApprovalHistory from "./approval-history/approval-history";
import ManualUploadPage from "./manual-upload/manual-upload-page";
import GdprPage from "./gdpr/gdpr";
import Tree from "./comparative-analysis/phylo/phylo";
import ComparativeAnalysis from "./comparative-analysis/comparative-analysis";
import "./style-reset.css";
import i18n from "./i18n";
import { Workspaces } from "./workspaces/workspaces";
import { Workspace } from "./workspaces/workspace";

export default function App() {
return (
Expand All @@ -38,6 +39,24 @@ export default function App() {
</Authorize>
)}
/>
<Route
exact
path="/workspaces"
render={() => (
<Authorize>
<Workspaces />
</Authorize>
)}
/>
<Route
exact
path="/workspaces/:id"
render={(params) => (
<Authorize>
<Workspace id={params.match.params.id} />
</Authorize>
)}
/>
<Route
exact
path="/gdpr"
Expand Down
4 changes: 4 additions & 0 deletions app/src/app/header/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CalendarIcon,
EditIcon,
LockIcon,
ViewIcon,
} from "@chakra-ui/icons";
import { Permission } from "sap-client";
import { IfPermission } from "auth/if-permission";
Expand All @@ -18,6 +19,9 @@ function NavBar() {
<NavLink to="/">
<Button leftIcon={<EditIcon />}>{t("Analysis results")}</Button>
</NavLink>
<NavLink to="/workspaces">
<Button leftIcon={<ViewIcon />}>{t("Workspaces")}</Button>
</NavLink>
<IfPermission permission={Permission.approve}>
<NavLink to="/approval-history">
<Button leftIcon={<CalendarIcon />}>
Expand Down
51 changes: 51 additions & 0 deletions app/src/app/workspaces/create-workspace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useCallback, useEffect, useState } from "react";
import { Button, useToast } from "@chakra-ui/react";
import { useMutation } from "redux-query-react";
import { useTranslation } from "react-i18next";
import { createWorkspace } from "./workspaces-query-configs";
import { AddIcon } from "@chakra-ui/icons";

export function CreateWorkspace() {
const { t } = useTranslation();
const toast = useToast();

const [
createWorkspaceQueryState,
createWorkspaceMutation,
] = useMutation((name: string) => createWorkspace({ name }));

const [needsNotify, setNeedsNotify] = useState(true);

const createWorkspaceCallback = useCallback(() => {
const name = prompt("Workspace name");
if (name) {
setNeedsNotify(true);
createWorkspaceMutation(name);
}
}, [createWorkspaceMutation, setNeedsNotify]);

useEffect(() => {
if (
needsNotify &&
createWorkspaceQueryState.status >= 200 &&
createWorkspaceQueryState.status < 300 &&
!createWorkspaceQueryState.isPending
) {
toast({
title: t("Workspace created"),
status: "info",
duration: 3000,
isClosable: true,
});
setNeedsNotify(false);
}
}, [t, createWorkspaceQueryState, toast, needsNotify, setNeedsNotify]);

return (
<div>
<Button leftIcon={<AddIcon />} onClick={() => createWorkspaceCallback()}>
{t("Create workspace")}
</Button>
</div>
);
}
Loading

0 comments on commit eaaebde

Please sign in to comment.