Skip to content

Commit

Permalink
Merge pull request #329 from visdesignlab/304-local-storage
Browse files Browse the repository at this point in the history
Update datatable dispatch to localforage and add loading spinner
  • Loading branch information
JakeWags authored Apr 1, 2024
2 parents 373e05e + 4061b79 commit e9ad950
Show file tree
Hide file tree
Showing 8 changed files with 1,047 additions and 1,357 deletions.
22 changes: 15 additions & 7 deletions e2e-tests/elementView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@ test.beforeEach(async ({ page }) => {

test('Element View', async ({ page }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');

// Make selection
const row = await page.locator('g > circle').first(); // row
await row.dispatchEvent('click');

// Open element view
await page.getByLabel('Open element view sidebar').click();

// test expansion buttons
await page.getByLabel('Expand the sidebar in full').click();
await page.getByLabel('Reduce the sidebar to normal').click();

const row = await page.locator('circle').first(); // row
await expect(row).toBeVisible();
await row.click();

// Ensure all headings are visible
const elementViewHeading = await page.getByRole('heading', { name: 'Element View' });
await expect(elementViewHeading).toBeVisible();
const elementQueriesHeading = await page.getByRole('heading', { name: 'Element Queries' });
Expand All @@ -50,25 +54,26 @@ test('Element View', async ({ page }) => {
const queryResultHeading = await page.getByRole('heading', { name: 'Query Result' });
await expect(queryResultHeading).toBeVisible();

// Check to see that the selection chip is visible
const selectionChip = await page.getByLabel('Selected intersection School');
await expect(selectionChip).toBeVisible();

// Check that the datatable is visible and populated
const dataTable = await page.locator('div').filter({ hasText: /^simpsons\/40726826Bart10simpsons\/40726838Ralph8simpsons\/40726848Martin Prince10$/ }).nth(2);
await expect(dataTable).toBeVisible();

const cell1 = await page.getByRole('cell', { name: 'simpsons/40726826' });
await expect(cell1).toBeVisible();

const cell2 = await page.getByRole('cell', { name: 'Bart' });
await expect(cell2).toBeVisible();

const cell3 = await page.getByRole('cell', { name: '10' }).first();
await expect(cell3).toBeVisible();

// Check that the add plot button is visible
const addPlot = await page.getByRole('button', { name: 'Add Plot' });
await expect(addPlot).toBeVisible();
await addPlot.click();

// Check that plot options and plot preview are visible
const histogramHeader = await page.getByRole('tab', { name: 'Histogram' });
await expect(histogramHeader).toBeVisible();
await histogramHeader.click();
Expand All @@ -85,16 +90,19 @@ test('Element View', async ({ page }) => {
const option3 = await page.locator('label').filter({ hasText: 'Frequency' });
await expect(option3).toBeVisible();

// close the plot preview
const closeButton = await page.getByRole('heading', { name: 'Add Plot' }).getByRole('button');
await expect(closeButton).toBeVisible();
await closeButton.click();

// Check that the download button is visible and works
const downloadPromise = page.waitForEvent('download');
const downloadButton = await page.getByLabel('Download 3 elements');
await expect(downloadButton).toBeVisible();
await downloadButton.click();
const download = await downloadPromise;

// Check that the close button is visible and works
const elementViewClose = await page.getByLabel('Close the sidebar');
await expect(elementViewClose).toBeVisible();
await elementViewClose.click();
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@types/react-dom": "^17.0.11",
"@visdesignlab/upset2-core": "^0.1.0",
"@visdesignlab/upset2-react": "^0.1.0",
"localforage": "^1.10.0",
"multinet": "0.23.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/atoms/loadingAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atom } from "recoil";

export const loadingAtom = atom({
key: 'loading-atom',
default: false,
});
30 changes: 19 additions & 11 deletions packages/app/src/components/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import React from 'react';
import { elementSidebarAtom } from '../atoms/elementSidebarAtom';
import { api } from '../atoms/authAtoms';
import { altTextSidebarAtom } from '../atoms/altTextSidebarAtom';
import { loadingAtom } from '../atoms/loadingAtom';
import { Backdrop, CircularProgress } from '@mui/material';

type Props = {
yOffset: number;
Expand All @@ -25,6 +27,7 @@ export const Body = ({ yOffset, data, config }: Props) => {
const [ isProvVisOpen, setIsProvVisOpen ] = useRecoilState(provenanceVisAtom);
const [ isElementSidebarOpen, setIsElementSidebarOpen ] = useRecoilState(elementSidebarAtom);
const [ isAltTextSidebarOpen, setIsAltTextSidebarOpen ] = useRecoilState(altTextSidebarAtom);
const loading = useRecoilValue(loadingAtom);

const provVis = {
open: isProvVisOpen,
Expand Down Expand Up @@ -70,17 +73,22 @@ export const Body = ({ yOffset, data, config }: Props) => {
<div style={{maxWidth: "100vw"}}>
{ data.setColumns.length === 0 ?
<ErrorModal />:
<Upset
data={data}
loadAttributes={3}
yOffset={yOffset === -1 ? 0 : yOffset}
extProvenance={provObject}
config={config}
provVis={provVis}
elementSidebar={elementSidebar}
altTextSidebar={altTextSidebar}
generateAltText={generateAltText}
/>
<div>
<Backdrop open={loading} style={{zIndex: 1000}}>
<CircularProgress color="inherit" />
</Backdrop>
<Upset
data={data}
loadAttributes={3}
yOffset={yOffset === -1 ? 0 : yOffset}
extProvenance={provObject}
config={config}
provVis={provVis}
elementSidebar={elementSidebar}
altTextSidebar={altTextSidebar}
generateAltText={generateAltText}
/>
</div>
}
</div>
);
Expand Down
169 changes: 97 additions & 72 deletions packages/app/src/components/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Box, Button } from "@mui/material"
import { Backdrop, Box, Button, CircularProgress } from "@mui/material"
import { AccessibleDataEntry, CoreUpsetData } from "@visdesignlab/upset2-core";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { getAccessibleData } from "@visdesignlab/upset2-react";
import DownloadIcon from '@mui/icons-material/Download';
import localforage from "localforage";

const getRowData = (row: AccessibleDataEntry) => {
return {id: row.id, elementName: `${(row.type === "Aggregate") ? "Aggregate: " : ""}${row.elementName.replaceAll("~&~", " & ")}`, size: row.size}
Expand Down Expand Up @@ -87,16 +88,34 @@ const DownloadButton = ({onClick}: DownloadButtonProps) => {
}

export const DataTable = () => {
const storedData = localStorage.getItem("data");
const storedRows = localStorage.getItem("rows");
const storedVisibleSets = localStorage.getItem("visibleSets");
const storedHiddenSets = localStorage.getItem("hiddenSets");

const data = storedData ? JSON.parse(storedData) as CoreUpsetData : null;
const rows = storedRows ? JSON.parse(storedRows) as ReturnType<typeof getAccessibleData> : null;
const visibleSets = storedVisibleSets ? JSON.parse(storedVisibleSets) as string[] : null;
const hiddenSets = storedHiddenSets ? JSON.parse(storedHiddenSets) as string[] : null;
const [data , setData] = useState<CoreUpsetData | null>(null);
const [rows, setRows] = useState<ReturnType<typeof getAccessibleData> | null>(null);
const [visibleSets, setVisibleSets] = useState<string[] | null>(null);
const [hiddenSets, setHiddenSets] = useState<string[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);

useEffect(() => {
setLoading(true);
Promise.all([
localforage.getItem("data"),
localforage.getItem("rows"),
localforage.getItem("visibleSets"),
localforage.getItem("hiddenSets")
]).then(([storedData, storedRows, storedVisibleSets, storedHiddenSets]) => {
if (storedData === null || storedRows === null || storedVisibleSets === null || storedHiddenSets === null) {
setError(true);
setLoading(false);
return;
}
setData(storedData as CoreUpsetData);
setRows(storedRows as ReturnType<typeof getAccessibleData>);
setVisibleSets(storedVisibleSets as string[]);
setHiddenSets(storedHiddenSets as string[]);
})
setLoading(false);
}, []);

// fetch subset data and create row objects with subset name and size
const tableRows: ReturnType<typeof getRowData>[] = useMemo(() => {
if (rows === null) {
Expand Down Expand Up @@ -182,68 +201,74 @@ export const DataTable = () => {

return (
<>
<Box sx={{display: "flex", justifyContent: "space-between"}}>
<Box sx={{width: "50%", margin: "20px"}}>
<div style={headerCSS}>
<h2>UpSet Data Table</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(tableRows, ["elementName", "size"], "upset2_datatable")} />
</div>
<DataGrid
columns={dataColumns}
rows={tableRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
{ error ?
<h1>Error fetching data...</h1> :
<Box sx={{display: "flex", justifyContent: "space-between"}}>
<Backdrop open={loading} style={{zIndex: 1000}}>
<CircularProgress color="inherit" />
</Backdrop>
<Box sx={{width: "50%", margin: "20px"}}>
<div style={headerCSS}>
<h2>UpSet Data Table</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(tableRows, ["elementName", "size"], "upset2_datatable")} />
</div>
<DataGrid
columns={dataColumns}
rows={tableRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
</Box>
<Box sx={{width: "25%", margin: "20px"}}>
<div style={headerCSS}>
<h2>Visible Sets</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(visibleSetRows, ["setName", "size"], "upset2_visiblesets_table")} />
</div>
<DataGrid
columns={setColumns}
rows={visibleSetRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
</Box>
<Box sx={{width: "25%", margin: "20px"}}>
<div style={headerCSS}>
<h2>Hidden Sets</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(hiddenSetRows, ["setName", "size"], "upset2_hiddensets_table")} />
</div>
<DataGrid
columns={setColumns}
rows={hiddenSetRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
</Box>
</Box>
<Box sx={{width: "25%", margin: "20px"}}>
<div style={headerCSS}>
<h2>Visible Sets</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(visibleSetRows, ["setName", "size"], "upset2_visiblesets_table")} />
</div>
<DataGrid
columns={setColumns}
rows={visibleSetRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
</Box>
<Box sx={{width: "25%", margin: "20px"}}>
<div style={headerCSS}>
<h2>Hidden Sets</h2>
<DownloadButton onClick={() => downloadElementsAsCSV(hiddenSetRows, ["setName", "size"], "upset2_hiddensets_table")} />
</div>
<DataGrid
columns={setColumns}
rows={hiddenSetRows}
autoHeight
disableSelectionOnClick
initialState={{
pagination: {
page: 0,
pageSize: 10,
},
}}
paginationMode="client"
rowsPerPageOptions={[5, 10, 20]}
></DataGrid>
</Box>
</Box>
}
</>
)
}
22 changes: 15 additions & 7 deletions packages/app/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { AccountCircle, ErrorOutline } from '@mui/icons-material';
import { AppBar, Avatar, Box, Button, ButtonGroup, IconButton, Menu, MenuItem, Toolbar, Tooltip, Typography } from '@mui/material';
import { useRecoilValue, useRecoilState } from 'recoil';
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
import React, { useContext, useEffect, useState } from 'react';
import localforage from 'localforage';
import { getMultinetDataUrl, oAuth } from '../../atoms/authAtoms';
import { getQueryParam, queryParamAtom, saveQueryParam } from '../../atoms/queryParamAtom';
import { provenanceVisAtom } from '../../atoms/provenanceVisAtom';
Expand All @@ -21,13 +22,15 @@ import { Link } from 'react-router-dom';
import { getUserInfo } from '../../getUserInfo';
import { restoreQueryParam } from '../../atoms/queryParamAtom';
import { altTextSidebarAtom } from '../../atoms/altTextSidebarAtom';
import { loadingAtom } from '../../atoms/loadingAtom';

const Header = ({ data }: { data: any }) => {
const { workspace } = useRecoilValue(queryParamAtom);
const [ isProvVisOpen, setIsProvVisOpen ] = useRecoilState(provenanceVisAtom);
const [ isElementSidebarOpen, setIsElementSidebarOpen ] = useRecoilState(elementSidebarAtom);
const [ isAltTextSidebarOpen, setIsAltTextSidebarOpen ] = useRecoilState(altTextSidebarAtom);
const importError = useRecoilValue(importErrorAtom);
const setLoading = useSetRecoilState(loadingAtom);

const { provenance } = useContext(ProvenanceContext);

Expand Down Expand Up @@ -99,14 +102,19 @@ const Header = ({ data }: { data: any }) => {
/**
* Dispatches the state by saving relevant data to the local storage.
* This function saves the 'data', 'rows', 'visibleSets', 'hiddenSets', and query parameters to the local storage.
* TODO: resolve cache size limit issues for large datasets (10mb)
*/
const dispatchState = () => {
localStorage.setItem('data', JSON.stringify(data));
localStorage.setItem('rows', JSON.stringify(getAccessibleData(getRows(data, provenance.getState()), true)));
localStorage.setItem('visibleSets', JSON.stringify(visibleSets));
localStorage.setItem('hiddenSets', JSON.stringify(hiddenSets.map((set: Column) => set.name)));
async function dispatchState() {
setLoading(true);
await Promise.all([
localforage.clear(),
localforage.setItem('data', data),
localforage.setItem('rows', getAccessibleData(getRows(data, provenance.getState()), true)),
localforage.setItem('visibleSets', visibleSets),
localforage.setItem('hiddenSets', hiddenSets.map((set: Column) => set.name))
]);

saveQueryParam();
setLoading(false);
};

const [ userInfo, setUserInfo ] = useState<UserSpec | null>(null);
Expand Down
Loading

0 comments on commit e9ad950

Please sign in to comment.