Skip to content

Commit

Permalink
Merge pull request #294 from bento-platform/feat/deduplicate-ind-expl…
Browse files Browse the repository at this point in the history
…orer

feat(explorer): deduplicate individual explorer entries
  • Loading branch information
davidlougheed authored Sep 26, 2023
2 parents 83eb184 + 9ad4cc9 commit 10fa2f8
Show file tree
Hide file tree
Showing 37 changed files with 1,409 additions and 1,041 deletions.
39 changes: 26 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:18-bullseye-slim AS build
FROM --platform=$BUILDPLATFORM node:20-bookworm-slim AS build

# Build bento_web with NodeJS + Webpack
# - Use BUILDPLATFORM for running webpack, since it should perform a lot better.
Expand All @@ -23,27 +23,40 @@ COPY static static

RUN npm run build

FROM nginx:1.23
FROM nginx:1.25

RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
# Install node so that we can run the create_config_prod.js & create_service_info.js scripts
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get update -y && \
apt-get install nodejs
apt-get install -y ca-certificates curl gnupg && \
mkdir -p /etc/apt/keyrings && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \
tee /etc/apt/sources.list.d/nodesource.list && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*

# Serve bento_web with NGINX; copy in configuration
COPY nginx.conf /etc/nginx/nginx.conf

WORKDIR /web
# Copy webpack-built source code from the build stage to the final image
COPY --from=build /web/dist ./dist
# Copy in package.json to provide version
COPY package.json .
# Copy in the production config generation script

# In general, we want to copy files in order of least -> most changed for layer caching reasons.

# - Copy in LICENSE so that people can see it if they explore the image contents
COPY LICENSE .
# - Copy in the production config generation script
COPY create_config_prod.js .
# Copy in the service info generator
# - Copy in the service info generator
COPY create_service_info.js .
# Copy in the entrypoint, which writes the config file and starts NGINX
# - Copy in the entrypoint, which writes the config file and starts NGINX
COPY run.bash .
# Copy in LICENSE so that people can see it if they explore the image contents
COPY LICENSE .
# - Copy in package.json to provide version to scripts
COPY package.json .
# - Copy webpack-built source code from the build stage to the final image
# - copy this last, since it changes more often than everything above it
# - this way we can cache layers
COPY --from=build /web/dist ./dist

CMD ["bash", "./run.bash"]
6 changes: 3 additions & 3 deletions dev.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:18-bullseye-slim AS install
FROM --platform=$BUILDPLATFORM node:20-bookworm-slim AS install

WORKDIR /web

Expand All @@ -7,15 +7,15 @@ COPY package-lock.json .

RUN npm ci

FROM ghcr.io/bento-platform/bento_base_image:node-debian-2023.03.22
FROM ghcr.io/bento-platform/bento_base_image:node-debian-2023.09.08

LABEL org.opencontainers.image.description="Local development image for Bento Web."

WORKDIR /web

COPY run.dev.bash .
COPY package.json .
COPY package-lock.json .
COPY run.dev.bash .

COPY --from=install /web/node_modules ./node_modules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import React, { useCallback } from "react";
import { Button } from "antd";
import PropTypes from "prop-types";

const DownloadButton = ({ disabled, uri, children }) => {
const DownloadButton = ({ disabled, uri, children, type }) => {
const { accessToken } = useSelector((state) => state.auth);

const onClick = useCallback(() => {
if (!uri) return;

const form = document.createElement("form");
form.method = "post";
form.target = "_blank";
form.action = uri;
form.innerHTML = `<input type="hidden" name="token" value="${accessToken}" />`;
document.body.appendChild(form);
Expand All @@ -24,21 +23,22 @@ const DownloadButton = ({ disabled, uri, children }) => {
}, [uri, accessToken]);

return (
<Button key="download" icon="download" disabled={disabled} onClick={onClick}>
{children}
<Button key="download" icon="download" type={type} disabled={disabled} onClick={onClick}>
{children === undefined ? "Download" : children}
</Button>
);
};

DownloadButton.defaultProps = {
disabled: false,
children: "Download",
type: "default",
};

DownloadButton.propTypes = {
disabled: PropTypes.bool,
uri: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
type: PropTypes.oneOf(["primary", "ghost", "dashed", "danger", "link", "default"]),
};

export default DownloadButton;
10 changes: 4 additions & 6 deletions src/components/datasets/DatasetDataTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@ const DatasetDataTypes = React.memo(
({isPrivate, project, dataset, onDatasetIngest}) => {
const dispatch = useDispatch();
const datasetDataTypes = useSelector((state) => Object.values(
state.datasetDataTypes.itemsById[dataset.identifier]?.itemsById ?? {}));
const datasetSummaries = useSelector((state) => state.datasetSummaries.itemsById[dataset.identifier]);
state.datasetDataTypes.itemsByID[dataset.identifier]?.itemsByID ?? {}));
const datasetSummaries = useSelector((state) => state.datasetSummaries.itemsByID[dataset.identifier]);
const isFetchingDataset = useSelector(
(state) => state.datasetDataTypes.itemsById[dataset.identifier]?.isFetching);
(state) => state.datasetDataTypes.itemsByID[dataset.identifier]?.isFetching);

const [datatypeSummaryVisible, setDatatypeSummaryVisible] = useState(false);
const [selectedDataType, setSelectedDataType] = useState(null);

const selectedSummary = (selectedDataType !== null && datasetSummaries)
? datasetSummaries[selectedDataType.id]
: {};
const selectedSummary = datasetSummaries?.data?.[selectedDataType?.id] ?? {};

const handleClearDataType = useCallback((dataType) => {
genericConfirm({
Expand Down
4 changes: 2 additions & 2 deletions src/components/datasets/DatasetOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {EM_DASH} from "../../constants";
import { useSelector } from "react-redux";

const DatasetOverview = ({isPrivate, project, dataset}) => {
const datasetsDataTypes = useSelector((state) => state.datasetDataTypes.itemsById);
const dataTypesSummary = Object.values(datasetsDataTypes[dataset.identifier]?.itemsById || {});
const datasetsDataTypes = useSelector((state) => state.datasetDataTypes.itemsByID);
const dataTypesSummary = Object.values(datasetsDataTypes[dataset.identifier]?.itemsByID || {});
const isFetchingDataset = datasetsDataTypes[dataset.identifier]?.isFetching;

// Count data types which actually have data in them for showing in the overview
Expand Down
2 changes: 1 addition & 1 deletion src/components/discovery/DiscoveryQueryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class DiscoveryQueryBuilder extends Component {
render() {
const { activeDataset, dataTypesByDataset} = this.props;

const dataTypesForActiveDataset = Object.values(dataTypesByDataset.itemsById[activeDataset] || {})
const dataTypesForActiveDataset = Object.values(dataTypesByDataset.itemsByID[activeDataset] || {})
.filter(dt => typeof dt === "object");

const filteredDataTypes = dataTypesForActiveDataset
Expand Down
61 changes: 33 additions & 28 deletions src/components/explorer/ExplorerDatasetSearch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useCallback, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";

Expand All @@ -22,6 +22,7 @@ import {
import IndividualsTable from "./searchResultsTables/IndividualsTable";
import BiosamplesTable from "./searchResultsTables/BiosamplesTable";
import ExperimentsTable from "./searchResultsTables/ExperimentsTable";
import {fetchDatasetResourcesIfNecessary} from "../../modules/datasets/actions";

const { TabPane } = Tabs;

Expand All @@ -36,42 +37,45 @@ const hasNonEmptyArrayProperty = (targetObject, propertyKey) => {
};

const ExplorerDatasetSearch = () => {
const { dataset } = useParams();
const { dataset: datasetID } = useParams();
const dispatch = useDispatch();

const datasetsByID = useSelector((state) => state.projects.datasetsByID);

const activeKey = useSelector((state) => state.explorer.activeTabByDatasetID[dataset]) || TAB_KEYS.INDIVIDUAL;
const dataTypeForms = useSelector((state) => state.explorer.dataTypeFormsByDatasetID[dataset] || []);
const fetchingSearch = useSelector((state) => state.explorer.fetchingSearchByDatasetID[dataset] || false);
const activeKey = useSelector((state) => state.explorer.activeTabByDatasetID[datasetID]) || TAB_KEYS.INDIVIDUAL;
const dataTypeForms = useSelector((state) => state.explorer.dataTypeFormsByDatasetID[datasetID] || []);
const fetchingSearch = useSelector((state) => state.explorer.fetchingSearchByDatasetID[datasetID] || false);
const fetchingTextSearch = useSelector((state) => state.explorer.fetchingTextSearch || false);
const searchResults = useSelector((state) => state.explorer.searchResultsByDatasetID[dataset] || null);
const searchResults = useSelector((state) => state.explorer.searchResultsByDatasetID[datasetID] || null);

console.debug("search results: ", searchResults);

const handleSetSelectedRows = (...args) => dispatch(setSelectedRows(dataset, ...args));
const handleSetSelectedRows = useCallback(
(...args) => dispatch(setSelectedRows(datasetID, ...args)),
[dispatch, datasetID],
);

useEffect(() => {
// Ensure user is at the top of the page after transition
window.scrollTo(0, 0);
}, []);

const onTabChange = (newActiveKey) => {
dispatch(setActiveTab(dataset, newActiveKey));
const onTabChange = useCallback((newActiveKey) => {
dispatch(setActiveTab(datasetID, newActiveKey));
handleSetSelectedRows([]);
};
}, [dispatch, datasetID, handleSetSelectedRows]);

const performSearch = () => {
dispatch(setActiveTab(dataset, TAB_KEYS.INDIVIDUAL));
dispatch(resetTableSortOrder(dataset));
dispatch(performSearchIfPossible(dataset));
};
const performSearch = useCallback(() => {
dispatch(setActiveTab(datasetID, TAB_KEYS.INDIVIDUAL));
dispatch(resetTableSortOrder(datasetID));
dispatch(performSearchIfPossible(datasetID));
}, [dispatch, datasetID]);

if (!dataset) return null;
useEffect(() => {
dispatch(fetchDatasetResourcesIfNecessary(datasetID));
}, [dispatch, datasetID]);

const selectedDataset = datasetsByID[dataset];

if (!selectedDataset) return null;
const selectedDataset = datasetsByID[datasetID];

const isFetchingSearchResults = fetchingSearch || fetchingTextSearch;

Expand All @@ -80,19 +84,20 @@ const ExplorerDatasetSearch = () => {
const hasBiosamples = hasNonEmptyArrayProperty(searchResults, "searchFormattedResultsBiosamples");
const showTabs = hasResults && (hasExperiments || hasBiosamples);

if (!selectedDataset) return null;
return (
<>
<Typography.Title level={4}>Explore Dataset {selectedDataset.title}</Typography.Title>
<SearchAllRecords datasetID={dataset} />
<SearchAllRecords datasetID={datasetID} />
<DiscoveryQueryBuilder
activeDataset={dataset}
activeDataset={datasetID}
isInternal={true}
dataTypeForms={dataTypeForms}
onSubmit={performSearch}
searchLoading={fetchingSearch}
addDataTypeQueryForm={(form) => dispatch(addDataTypeQueryForm(dataset, form))}
updateDataTypeQueryForm={(index, form) => dispatch(updateDataTypeQueryForm(dataset, index, form))}
removeDataTypeQueryForm={(index) => dispatch(removeDataTypeQueryForm(dataset, index))}
addDataTypeQueryForm={(form) => dispatch(addDataTypeQueryForm(datasetID, form))}
updateDataTypeQueryForm={(index, form) => dispatch(updateDataTypeQueryForm(datasetID, index, form))}
removeDataTypeQueryForm={(index) => dispatch(removeDataTypeQueryForm(datasetID, index))}
/>
{hasResults &&
!isFetchingSearchResults &&
Expand All @@ -101,28 +106,28 @@ const ExplorerDatasetSearch = () => {
<TabPane tab="Individual" key={TAB_KEYS.INDIVIDUAL}>
<IndividualsTable
data={searchResults.searchFormattedResults}
datasetID={dataset}
datasetID={datasetID}
/>
</TabPane>
{hasBiosamples && (
<TabPane tab="Biosamples" key={TAB_KEYS.BIOSAMPLES}>
<BiosamplesTable
data={searchResults.searchFormattedResultsBiosamples}
datasetID={dataset}
datasetID={datasetID}
/>
</TabPane>
)}
{hasExperiments && (
<TabPane tab="Experiments" key={TAB_KEYS.EXPERIMENTS}>
<ExperimentsTable
data={searchResults.searchFormattedResultsExperiment}
datasetID={dataset}
datasetID={datasetID}
/>
</TabPane>
)}
</Tabs>
) : (
<IndividualsTable data={searchResults.searchFormattedResults} datasetID={dataset} />
<IndividualsTable data={searchResults.searchFormattedResults} datasetID={datasetID} />
))}
</>
);
Expand Down
Loading

0 comments on commit 10fa2f8

Please sign in to comment.