diff --git a/packages/web/src/components/DatasetDetails/DatasetDetails.module.css b/packages/web/src/components/DatasetDetails/DatasetDetails.module.css index d486b517c..826d7ef26 100644 --- a/packages/web/src/components/DatasetDetails/DatasetDetails.module.css +++ b/packages/web/src/components/DatasetDetails/DatasetDetails.module.css @@ -9,9 +9,8 @@ position: absolute; top: 50px; right: 0; - /* filter: drop-shadow(black 0px 0px); */ padding: 24px 30px; - border-left: 1px solid var(--borders-light-grey); + border-left: 1px solid var(--medium-grey); box-shadow: -4px 10px 8px 0px #000000 } @@ -24,15 +23,6 @@ position: absolute; top: 0; right: 0; - color: var(--primary-text-color); - font-weight: 500; -} - -.close-button:hover, .close-button:focus { - background-color: var(--aqua); - background: var(--aqua); - color: var(--white); - font-weight: 600; } .title { @@ -45,8 +35,8 @@ hr { border: none; height: 1px !important; - color: var(--borders-light-grey); /* old IE */ - background-color: var(--borders-light-grey); /* Modern Browsers */ + color: var(--medium-grey); /* old IE */ + background-color: var(--medium-grey); /* Modern Browsers */ } .content { @@ -97,49 +87,15 @@ hr { } .button { - background-color: var(--aqua); - color: var(--white); - border: 1px solid var(--secondary-dark); - border-radius: 18px; height: 33px; + width: fit-content; margin: var(--margin) 0; } -.button:hover, -.button:focus { - background-color: var(--bright-aqua); - color: var(--white); -} - -.button-label { - font-size: 14px; - font-weight: 400; -} - -.button-icon { - font-size: 15px; - font-weight: 500; -} - .secondary-close-button { - background-color: none; - background: none; - color: var(--aqua); - border: 1px solid var(--aqua); - border-radius: 18px; height: 33px; + width: fit-content; position: absolute; bottom: 0; right: 0; } - -.secondary-close-button-label { - font-weight: 400; - font-size: 14px; -} - -.secondary-close-button:hover, .secondary-close-button:focus { - background-color: var(--aqua-secondary-hover); - background: var(--aqua-secondary-hover); - color: var(--bright-aqua) -} diff --git a/packages/web/src/components/DatasetDetails/DatasetDetailsRow.module.css b/packages/web/src/components/DatasetDetails/DatasetDetailsRow.module.css index 9bdbe4c6b..9e0923649 100644 --- a/packages/web/src/components/DatasetDetails/DatasetDetailsRow.module.css +++ b/packages/web/src/components/DatasetDetails/DatasetDetailsRow.module.css @@ -31,3 +31,13 @@ /* Allow whitespace to be rendered (including carriage returns & new lines) */ white-space: pre-wrap; } + +.link { + color: var(--aqua); +} + +.link:hover { + text-decoration: underline; + color: var(--bright-aqua); +} + diff --git a/packages/web/src/components/DatasetDetails/DatasetDetailsRow.tsx b/packages/web/src/components/DatasetDetails/DatasetDetailsRow.tsx index 4c5448934..340ddbcea 100644 --- a/packages/web/src/components/DatasetDetails/DatasetDetailsRow.tsx +++ b/packages/web/src/components/DatasetDetails/DatasetDetailsRow.tsx @@ -9,6 +9,7 @@ interface DatasetMetadataRowProps { className?: string; name: string; value: string; + link?: string; } /** @@ -19,10 +20,16 @@ export default function DatasetDetailsRow(props: DatasetMetadataRowProps) { return (
- {props.name} + {props.name} - {props.value} + {props?.link ? ( + + {props.value} + + ) : ( + {props.value} + )}
); diff --git a/packages/web/src/components/DatasetDetails/index.tsx b/packages/web/src/components/DatasetDetails/index.tsx index 851eba895..61cc0ccb5 100644 --- a/packages/web/src/components/DatasetDetails/index.tsx +++ b/packages/web/src/components/DatasetDetails/index.tsx @@ -1,4 +1,3 @@ -import { DefaultButton, IconButton } from "@fluentui/react"; import classNames from "classnames"; import { get as _get } from "lodash"; import * as React from "react"; @@ -6,8 +5,16 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import DatasetDetailsRow from "./DatasetDetailsRow"; -import PublicDataset, { DATASET_DISPLAY_FIELDS } from "../../entity/PublicDataset"; +import PublicDataset, { + DATASET_DISPLAY_FIELDS, + DatasetAnnotations, +} from "../../entity/PublicDataset"; import { interaction, selection } from "../../../../core/state"; +import { + PrimaryButton, + SecondaryButton, + TertiaryButton, +} from "../../../../core/components/Buttons"; import { getNameAndTypeFromSourceUrl, Source } from "../../../../core/entity/FileExplorerURL"; import styles from "./DatasetDetails.module.css"; @@ -36,8 +43,15 @@ export default function DatasetDetails() { return DATASET_DISPLAY_FIELDS.reduce((accum, field) => { const fieldName = field.name; let datasetFieldValue; + let link; if (datasetDetails.details.hasOwnProperty(fieldName)) { datasetFieldValue = _get(datasetDetails.details, fieldName); + if ( + (fieldName === DatasetAnnotations.RELATED_PUBLICATON.name || + fieldName === DatasetAnnotations.DOI.name) && + datasetDetails.details.hasOwnProperty(DatasetAnnotations.DOI.name) + ) + link = _get(datasetDetails.details, DatasetAnnotations.DOI.name); } else datasetFieldValue = "--"; // Still display field, just indicate no value provided const ret = [ ...accum, @@ -46,6 +60,7 @@ export default function DatasetDetails() { className={styles.row} name={field.displayLabel} value={datasetFieldValue} + link={link || undefined} />, ]; return ret; @@ -88,21 +103,16 @@ export default function DatasetDetails() { })} >
- dispatch(interaction.actions.hideDatasetDetailsPanel())} />
{datasetDetails?.name}
- {isLongDescription && toggleDescriptionButton}
{content}
- dispatch(interaction.actions.hideDatasetDetailsPanel())} diff --git a/packages/web/src/components/DatasetDetails/test/DatasetDetails.test.tsx b/packages/web/src/components/DatasetDetails/test/DatasetDetails.test.tsx index d1d8784c1..415d8cb06 100644 --- a/packages/web/src/components/DatasetDetails/test/DatasetDetails.test.tsx +++ b/packages/web/src/components/DatasetDetails/test/DatasetDetails.test.tsx @@ -72,6 +72,28 @@ describe("", () => { expect(getByText(mockDataset.name)).to.exist; expect(getByText(mockDataset.description)).to.exist; }); + it("renders links for ref publication and DOI if provided", () => { + const mockDataset = makePublicDatasetMock("test-id"); + + // Arrange + const { store } = configureMockStore({ + state: mergeState(initialState, { + interaction: { + selectedPublicDataset: mockDataset, + }, + }), + }); + const { getAllByRole } = render( + + + + ); + + expect(getAllByRole("link").length).to.equal(2); + expect(getAllByRole("link").at(0)?.getAttribute("href")).to.equal( + mockDataset.details.doi + ); + }); it("displays indicator for undefined fields", () => { const sparseDataset = new PublicDataset({ dataset_name: "Sparse Dataset", diff --git a/packages/web/src/components/OpenSourceDatasets/DatasetColumns.ts b/packages/web/src/components/OpenSourceDatasets/DatasetColumns.ts deleted file mode 100644 index a04a60813..000000000 --- a/packages/web/src/components/OpenSourceDatasets/DatasetColumns.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { IColumn } from "@fluentui/react"; -import { DatasetAnnotations } from "../../entity/PublicDataset"; - -// TO DO: Use better pattern to avoid so many constants -export const DatasetColumns: IColumn[] = [ - { - key: "column1", - name: DatasetAnnotations.DATASET_NAME.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.DATASET_NAME.name, - ariaLabel: "Column operations for File type, Press to sort on File type", - isResizable: true, - minWidth: 50, - }, - { - key: "column2", - name: DatasetAnnotations.CREATION_DATE.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.CREATION_DATE.name, - minWidth: 112, - isResizable: true, - }, - { - key: "column3", - name: DatasetAnnotations.RELATED_PUBLICATON.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.RELATED_PUBLICATON.name, - minWidth: 178, - isResizable: true, - }, - { - key: "column4", - name: DatasetAnnotations.PUBLICATION_DATE.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.PUBLICATION_DATE.name, - minWidth: 128, - isResizable: true, - }, - { - key: "column5", - name: DatasetAnnotations.FILE_COUNT.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.FILE_COUNT.name, - minWidth: 89, - isResizable: true, - data: "number", - }, - { - key: "column6", - name: DatasetAnnotations.DATASET_SIZE.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.DATASET_SIZE.name, - minWidth: 78, - isResizable: true, - }, - { - key: "column7", - name: DatasetAnnotations.DATASET_DESCRIPTION.displayLabel.toUpperCase(), - fieldName: DatasetAnnotations.DATASET_DESCRIPTION.name, - minWidth: 200, - isResizable: true, - }, -]; diff --git a/packages/web/src/components/OpenSourceDatasets/DatasetRow.module.css b/packages/web/src/components/OpenSourceDatasets/DatasetRow.module.css index 1e243acca..a11ac3d18 100644 --- a/packages/web/src/components/OpenSourceDatasets/DatasetRow.module.css +++ b/packages/web/src/components/OpenSourceDatasets/DatasetRow.module.css @@ -6,7 +6,7 @@ color: var(--secondary-text-color); background-color: var(--secondary-dark); background: var(--secondary-dark) !important; - border-bottom: 1px solid var(--borders-light-grey); + border-bottom: 1px solid var(--medium-grey); font-family: var(--font-family); font-size: 14px; font-weight: 400; @@ -35,26 +35,11 @@ } .button { - background-color: var(--aqua); - color: var(--white); - border: 1px solid var(--secondary-dark); - border-radius: 18px; height: 33px; - margin-right: 5px; + width: fit-content; + margin-right: 5px; } -.button:hover, -.button:focus { - background-color: var(--bright-aqua); - color: var(--white); -} - -.button-label { - font-size: 14px; - font-weight: 400; -} - -.button-icon { - font-size: 15px; - font-weight: 500; +.button span { + overflow-x: clip; } diff --git a/packages/web/src/components/OpenSourceDatasets/DatasetRow.tsx b/packages/web/src/components/OpenSourceDatasets/DatasetRow.tsx index 707b4f5b0..55ecd477d 100644 --- a/packages/web/src/components/OpenSourceDatasets/DatasetRow.tsx +++ b/packages/web/src/components/OpenSourceDatasets/DatasetRow.tsx @@ -1,12 +1,13 @@ -import { DefaultButton, IDetailsRowProps, IRenderFunction } from "@fluentui/react"; +import { IDetailsRowProps, IRenderFunction } from "@fluentui/react"; import classNames from "classnames"; import * as React from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import PublicDataset from "../../entity/PublicDataset"; import { interaction, selection } from "../../../../core/state"; import { getNameAndTypeFromSourceUrl, Source } from "../../../../core/entity/FileExplorerURL"; +import { PrimaryButton } from "../../../../core/components/Buttons"; import styles from "./DatasetRow.module.css"; @@ -20,6 +21,7 @@ export default function DatasetRow(props: DatasetRowProps) { const navigate = useNavigate(); const [showActions, setShowActions] = React.useState(true); const dataset = new PublicDataset(props.rowProps.item); + const currentGlobalURL = useSelector(selection.selectors.getEncodedFileExplorerUrl); const selectDataset = () => { dispatch(interaction.actions.setSelectedPublicDataset(dataset)); @@ -27,13 +29,16 @@ export default function DatasetRow(props: DatasetRowProps) { }; const openDatasetInApp = (source: Source) => { - navigate("/app"); dispatch( selection.actions.addQuery({ name: `New ${source.name} Query on ${dataset?.name || "open-source dataset"}`, parts: { sources: [source] }, }) ); + navigate({ + pathname: "/app", + search: `?${currentGlobalURL}`, + }); }; const loadDataset = () => { @@ -64,23 +69,15 @@ export default function DatasetRow(props: DatasetRowProps) { [styles.buttonWrapperHidden]: showActions, })} > - - div { background-color: var(--secondary-dark); background: var(--secondary-dark); - border-bottom: 1px solid var(--borders-light-grey); + border-bottom: 1px solid var(--medium-grey); } .table-header span { @@ -15,10 +15,11 @@ font-weight: 400; } -.table-header > div:hover, .table-header span:hover { - background-color: var(--secondary-dark); - background: var(--secondary-dark); +.table-header span:hover { + background-color: var(--primary-dark); + background: var(--primary-dark); color: var(--secondary-text-color); + cursor: pointer; } .double-line { @@ -42,3 +43,12 @@ font-size: 14px; text-align: center; } + +.link { + color: var(--aqua); +} + +.link:hover { + text-decoration: underline; + color: var(--bright-aqua); +} diff --git a/packages/web/src/components/OpenSourceDatasets/DatasetTable.tsx b/packages/web/src/components/OpenSourceDatasets/DatasetTable.tsx index d3eba3957..a88494422 100644 --- a/packages/web/src/components/OpenSourceDatasets/DatasetTable.tsx +++ b/packages/web/src/components/OpenSourceDatasets/DatasetTable.tsx @@ -8,13 +8,18 @@ import { ThemeProvider, } from "@fluentui/react"; import { ShimmeredDetailsList } from "@fluentui/react/lib/ShimmeredDetailsList"; +import classNames from "classnames"; import * as React from "react"; -import { DatasetColumns } from "./DatasetColumns"; import DatasetRow from "./DatasetRow"; import useDatasetDetails from "./useDatasetDetails"; -import { PublicDatasetProps } from "../../entity/PublicDataset"; +import { + PublicDatasetProps, + DATASET_TABLE_FIELDS, + DatasetAnnotations, +} from "../../entity/PublicDataset"; import FileFilter from "../../../../core/entity/FileFilter"; +import FileSort, { SortOrder } from "../../../../core/entity/FileSort"; import styles from "./DatasetTable.module.css"; @@ -23,9 +28,25 @@ interface DatasetTableProps { } export default function DatasetTable(props: DatasetTableProps) { - const [datasetDetails, isLoading, error] = useDatasetDetails(props?.filters || []); - - const items = datasetDetails?.map((detail) => detail.details); + const [sortColumn, setSortColumn] = React.useState(undefined); + const columns = DATASET_TABLE_FIELDS.map( + (value, index): IColumn => { + return { + key: `column${index}`, + name: value.displayLabel.toUpperCase(), + fieldName: value.name, + isResizable: true, + minWidth: value?.minWidth, + isSorted: sortColumn?.annotationName == value.displayLabel, + isSortedDescending: sortColumn?.order == SortOrder.DESC, + onColumnClick: () => onColumnClick(value.displayLabel), + }; + } + ); + const [datasetDetails, isLoading, error] = useDatasetDetails(props?.filters || [], sortColumn); + const items = React.useMemo(() => { + return datasetDetails?.map((detail) => detail.details); + }, [datasetDetails]); const renderRow = ( rowProps: IDetailsRowProps | undefined, @@ -41,7 +62,7 @@ export default function DatasetTable(props: DatasetTableProps) { const globalStyle = getComputedStyle(document.body); const shimmeredDetailsListTheme: PartialTheme = createTheme({ semanticColors: { - disabledBackground: globalStyle.getPropertyValue("--borders-light-grey"), + disabledBackground: globalStyle.getPropertyValue("--medium-grey"), bodyBackground: globalStyle.getPropertyValue("--secondary-dark"), bodyDivider: globalStyle.getPropertyValue("--primary-dark"), }, @@ -54,16 +75,31 @@ export default function DatasetTable(props: DatasetTableProps) { ) { const fieldContent = item[column?.fieldName as keyof PublicDatasetProps] as string; if (!fieldContent) return <>--; + if (column?.fieldName === DatasetAnnotations.RELATED_PUBLICATON.name && item?.doi) { + return ( + + {fieldContent} + + ); + } return
{fieldContent}
; } + function onColumnClick(columnName: string) { + let sortOrder = SortOrder.ASC; + if (sortColumn?.annotationName == columnName) + sortOrder = sortColumn.order == SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC; + const newSortColumn = new FileSort(columnName, sortOrder); + setSortColumn(newSortColumn); + } + return (
", () => { const sandbox = createSandbox(); @@ -82,7 +83,7 @@ describe("", () => { ); // Act / Assert - expect(getAllByRole("columnheader").length).to.equal(DatasetColumns.length); + expect(getAllByRole("columnheader").length).to.equal(DATASET_TABLE_FIELDS.length); }); it("renders rows for each dataset", () => { @@ -102,4 +103,35 @@ describe("", () => { expect(getAllByRole("row").length).to.equal(mockIdList.length + 1); expect(useDatasetDetailsStub.called).to.be.true; }); + + it("sorts on column header click", async () => { + const getSpy = sandbox.spy(useDatasetDetails, "default"); + + const { getByText } = render( + + + + ); + + const mockFileSortAsc = new FileSort( + DatasetAnnotations.DATASET_NAME.displayLabel, + SortOrder.ASC + ); + const mockFileSortDesc = new FileSort( + DatasetAnnotations.DATASET_NAME.displayLabel, + SortOrder.DESC + ); + const mockFileSortAscCount = new FileSort( + DatasetAnnotations.FILE_COUNT.displayLabel, + SortOrder.ASC + ); + + // Act / Assert + fireEvent.click(getByText(/Dataset Name/i)); + expect(getSpy.calledWith([], mockFileSortAsc)).to.be.true; + fireEvent.click(getByText(/Dataset Name/i)); + expect(getSpy.calledWith([], mockFileSortDesc)).to.be.true; + fireEvent.click(getByText(/File count/i)); + expect(getSpy.calledWith([], mockFileSortAscCount)).to.be.true; + }); }); diff --git a/packages/web/src/components/OpenSourceDatasets/useDatasetDetails.ts b/packages/web/src/components/OpenSourceDatasets/useDatasetDetails.ts index 08720f9fb..5f9fa0865 100644 --- a/packages/web/src/components/OpenSourceDatasets/useDatasetDetails.ts +++ b/packages/web/src/components/OpenSourceDatasets/useDatasetDetails.ts @@ -5,6 +5,7 @@ import PublicDataset from "../../entity/PublicDataset"; import FileSet from "../../../../core/entity/FileSet"; import { interaction } from "../../../../core/state"; import FileFilter from "../../../../core/entity/FileFilter"; +import FileSort from "../../../../core/entity/FileSort"; /** * Custom React hook to accomplish storing and fetching details of datasets (i.e., dataset metadata). @@ -12,7 +13,8 @@ import FileFilter from "../../../../core/entity/FileFilter"; * of the return array will be true. */ export default function useDatasetDetails( - filters: FileFilter[] + filters: FileFilter[], + fileSort?: FileSort | undefined ): [PublicDataset[] | null, boolean, string | undefined] { const [isLoading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(); @@ -26,8 +28,9 @@ export default function useDatasetDetails( return new FileSet({ fileService: publicDatasetListService, filters, + sort: fileSort, }); - }, [publicDatasetListService, filters]); + }, [publicDatasetListService, filters, fileSort]); React.useEffect(() => { setIsLoading(true); if (!!publicDatasetListService && !!fileSet) { diff --git a/packages/web/src/entity/PublicDataset/index.ts b/packages/web/src/entity/PublicDataset/index.ts index 590a3e4b1..b145f36a5 100644 --- a/packages/web/src/entity/PublicDataset/index.ts +++ b/packages/web/src/entity/PublicDataset/index.ts @@ -20,60 +20,40 @@ export interface PublicDatasetProps { source?: string; // Indicate whether the dataset comes from internal (AICS) or external (other) source } +export class DatasetAnnotation { + public displayLabel: string; + public name: string; + public minWidth: number; + + constructor(displayLabel: string, name: string, minWidth = 0) { + this.displayLabel = displayLabel; + this.name = name; + this.minWidth = minWidth; + } + + public equals(target: DatasetAnnotation): boolean { + return this.displayLabel === target.displayLabel && this.name === target.name; + } +} + // Originally noted in core/entity/Annotation: TypeScript (3.9) raises an error if this is an enum // Reference issue without clear resolution: https://github.com/microsoft/TypeScript/issues/6307 /** * Matches fields in the dataset manifest to props in this interface */ export const DatasetAnnotations = { - CREATION_DATE: { - displayLabel: "Creation date", - name: "created", - }, - DATASET_ID: { - displayLabel: "Dataset ID", - name: "dataset_id", - }, - DATASET_NAME: { - displayLabel: "Dataset name", - name: "dataset_name", - }, - DATASET_PATH: { - displayLabel: "File Path", - name: "dataset_path", - }, - DATASET_SIZE: { - displayLabel: "Size", - name: "dataset_size", - }, - DATASET_DESCRIPTION: { - displayLabel: "Short description", - name: "description", - }, - DOI: { - displayLabel: "DOI", - name: "doi", - }, - FILE_COUNT: { - displayLabel: "File count", - name: "file_count", - }, - PUBLICATION_DATE: { - displayLabel: "Publication date", - name: "published", - }, - RELATED_PUBLICATON: { - displayLabel: "Related publication", - name: "related_publication", - }, - SOURCE: { - displayLabel: "Source", - name: "source", - }, - VERSION: { - displayLabel: "Version", - name: "version", - }, + CREATION_DATE: new DatasetAnnotation("Creation date", "created", 112), + DATASET_ID: new DatasetAnnotation("Dataset ID", "dataset_id"), + DATASET_NAME: new DatasetAnnotation("Dataset name", "dataset_name", 50), + DATASET_PATH: new DatasetAnnotation("File Path", "dataset_path"), + DATASET_SIZE: new DatasetAnnotation("Size", "dataset_size", 78), + DATASET_DESCRIPTION: new DatasetAnnotation("Short description", "description", 200), + DOI: new DatasetAnnotation("DOI", "doi"), + FILE_COUNT: new DatasetAnnotation("File count", "file_count", 89), + PUBLICATION_DATE: new DatasetAnnotation("Publication date", "published", 128), + RELATED_PUBLICATON: new DatasetAnnotation("Related publication", "related_publication", 178), + SOURCE: new DatasetAnnotation("Source", "source"), + VERSION: new DatasetAnnotation("Version", "version"), }; // Limited set used for the details panel @@ -86,6 +66,17 @@ export const DATASET_DISPLAY_FIELDS = [ DatasetAnnotations.DOI, ]; +// Limited set used for the table header +export const DATASET_TABLE_FIELDS = [ + DatasetAnnotations.DATASET_NAME, + DatasetAnnotations.CREATION_DATE, + DatasetAnnotations.RELATED_PUBLICATON, + DatasetAnnotations.PUBLICATION_DATE, + DatasetAnnotations.FILE_COUNT, + DatasetAnnotations.DATASET_SIZE, + DatasetAnnotations.DATASET_DESCRIPTION, +]; + /** * Handles conversion from an FmsFile to a dataset format that can be accepted by IDetailsRow */ diff --git a/packages/web/src/services/DatabaseServiceWeb.ts b/packages/web/src/services/DatabaseServiceWeb.ts index b4b9d5e74..17316036b 100644 --- a/packages/web/src/services/DatabaseServiceWeb.ts +++ b/packages/web/src/services/DatabaseServiceWeb.ts @@ -97,7 +97,7 @@ export default class DatabaseServiceWeb extends DatabaseService { duckdb.DuckDBDataProtocol.BROWSER_FILEREADER, true ); - } else if ((uri as any) instanceof String) { + } else if (typeof uri === "string") { const protocol = uri.startsWith("s3") ? duckdb.DuckDBDataProtocol.S3 : duckdb.DuckDBDataProtocol.HTTP;