Skip to content

Commit

Permalink
feat: add dataset card + show datasets in project overview
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlougheed committed Oct 23, 2024
1 parent 6c47d5a commit 9e27258
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 57 deletions.
7 changes: 4 additions & 3 deletions src/js/components/BentoAppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Routes, Route, useNavigate, useParams, Outlet } from 'react-router-dom';
import { useAutoAuthenticate, useIsAuthenticated } from 'bento-auth-js';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { useAppDispatch } from '@/hooks';

import { makeGetConfigRequest, makeGetServiceInfoRequest } from '@/features/config/config.store';
import { makeGetAboutRequest } from '@/features/content/content.store';
Expand All @@ -11,6 +11,7 @@ import { getBeaconConfig } from '@/features/beacon/beacon.store';
import { getBeaconNetworkConfig } from '@/features/beacon/network.store';
import { fetchGohanData, fetchKatsuData } from '@/features/ingestion/lastIngestion.store';
import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store';
import { useMetadata } from '@/features/metadata/hooks';
import { getProjects, markScopeSet, selectScope } from '@/features/metadata/metadata.store';

import Loader from '@/components/Loader';
Expand All @@ -29,7 +30,7 @@ const ScopedRoute = () => {
const { projectId, datasetId } = useParams();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { selectedScope, projects } = useAppSelector((state) => state.metadata);
const { selectedScope, projects } = useMetadata();

useEffect(() => {
// Update selectedScope based on URL parameters
Expand Down Expand Up @@ -76,7 +77,7 @@ const BentoAppRouter = () => {

const { isAutoAuthenticating } = useAutoAuthenticate();
const isAuthenticated = useIsAuthenticated();
const { selectedScope, isFetching: isFetchingProjects } = useAppSelector((state) => state.metadata);
const { selectedScope, isFetching: isFetchingProjects } = useMetadata();

useEffect(() => {
if (!selectedScope.scopeSet) return;
Expand Down
4 changes: 2 additions & 2 deletions src/js/components/Overview/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { BarChart, Histogram, PieChart } from 'bento-charts';
import { ChoroplethMap } from 'bento-charts/dist/maps';

import { CHART_HEIGHT, PIE_CHART_HEIGHT } from '@/constants/overviewConstants';
import { useSelectedScope } from '@/features/metadata/hooks';
import type { ChartData } from '@/types/data';
import type { ChartConfig } from '@/types/chartConfig';
import { CHART_TYPE_BAR, CHART_TYPE_HISTOGRAM, CHART_TYPE_CHOROPLETH, CHART_TYPE_PIE } from '@/types/chartConfig';
import { useAppSelector } from '@/hooks';
import { scopeToUrl } from '@/utils/router';

interface ChartEvent {
Expand All @@ -19,7 +19,7 @@ interface ChartEvent {
const Chart = memo(({ chartConfig, data, units, id, isClickable }: ChartProps) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { scope } = useAppSelector((state) => state.metadata.selectedScope);
const { scope } = useSelectedScope();
const translateMap = ({ x, y }: { x: string; y: number }) => ({ x: t(x), y });
const removeMissing = ({ x }: { x: string }) => x !== 'missing';

Expand Down
21 changes: 5 additions & 16 deletions src/js/components/Overview/ChartCard.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
import type React from 'react';
import { memo, useRef } from 'react';
import { Button, Card, Row, Space, Tooltip, Typography } from 'antd';
import { Button, Card, Row, Space, Tooltip } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import Chart from './Chart';
import CustomEmpty from '../Util/CustomEmpty';
import { BOX_SHADOW, CHART_HEIGHT } from '@/constants/overviewConstants';
import { useElementWidth, useTranslationCustom, useTranslationDefault } from '@/hooks';
import type { ChartDataField } from '@/types/data';
import SmallChartCardTitle from '@/components/Util/SmallChartCardTitle';

const CARD_STYLE: React.CSSProperties = { height: '415px', borderRadius: '11px', ...BOX_SHADOW };
const ROW_EMPTY_STYLE: React.CSSProperties = { height: `${CHART_HEIGHT}px` };

interface TitleComponentProps {
title: string;
description: string;
}

const TitleComponent: React.FC<TitleComponentProps> = ({ title, description }) => (
<Space.Compact direction="vertical" style={{ fontWeight: 'normal', padding: '5px 5px' }}>
<Typography.Text style={{ fontSize: '20px', fontWeight: '600' }}>{title}</Typography.Text>
<Typography.Text type="secondary" style={{ width: '375px' }} ellipsis={{ tooltip: description }}>
{description}
</Typography.Text>
</Space.Compact>
);

const ChartCard: React.FC<ChartCardProps> = memo(({ section, chart, onRemoveChart }) => {
const t = useTranslationCustom();
const td = useTranslationDefault();
Expand All @@ -51,7 +38,9 @@ const ChartCard: React.FC<ChartCardProps> = memo(({ section, chart, onRemoveChar
return (
<div ref={containerRef} key={id} style={{ gridColumn: `span ${width}` }}>
<Card
title={<TitleComponent title={t(title)} description={t(description)} />}
title={
<SmallChartCardTitle title={t(title)} description={t(description)} descriptionStyle={{ width: '375px' }} />
}
style={CARD_STYLE}
size="small"
extra={
Expand Down
22 changes: 21 additions & 1 deletion src/js/components/Overview/PublicOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { Row, Col, FloatButton, Card, Skeleton } from 'antd';
import { Row, Col, FloatButton, Card, Skeleton, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons';

import { convertSequenceAndDisplayData, saveValue } from '@/utils/localStorage';
Expand All @@ -14,7 +14,9 @@ import LastIngestionInfo from './LastIngestion';
import Loader from '@/components/Loader';

import { useAppSelector } from '@/hooks';
import { useSelectedProject, useSelectedScope } from '@/features/metadata/hooks';
import { useTranslation } from 'react-i18next';
import Dataset from '@/components/Provenance/Dataset';

const ABOUT_CARD_STYLE = { width: '100%', maxWidth: '1390px', borderRadius: '11pX', ...BOX_SHADOW };
const MANAGE_CHARTS_BUTTON_STYLE = { right: '5em', bottom: '1.5em', transform: 'scale(125%)' };
Expand All @@ -32,6 +34,9 @@ const PublicOverview = () => {
} = useAppSelector((state) => state.data);
const { isFetchingAbout, about } = useAppSelector((state) => state.content);

const selectedProject = useSelectedProject();
const { scope } = useSelectedScope();

useEffect(() => {
// Save sections to localStorage when they change
if (isFetchingOverviewData) return;
Expand Down Expand Up @@ -69,6 +74,21 @@ const PublicOverview = () => {
<Counts />
</Col>
</Row>
{selectedProject && !scope.dataset && (
// If we have a project with more than one dataset, show a dataset selector in the project overview
<Row>
<Col flex={1}>
<Typography.Title level={3}>Datasets</Typography.Title>
<Row gutter={[12, 12]}>
{selectedProject.datasets.map((d) => (
<Col key={d.identifier} style={{ width: '100%', maxWidth: 360 }}>
<Dataset parentProjectID={selectedProject.identifier} dataset={d} format="small-card" />
</Col>
))}
</Row>
</Col>
</Row>
)}
<Row>
<Col flex={1}>
{displayedSections.map(({ sectionTitle, charts }, i) => (
Expand Down
67 changes: 67 additions & 0 deletions src/js/components/Provenance/Dataset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { Avatar, Button, Card, List } from 'antd';
import { FaDatabase } from 'react-icons/fa';

import type { DiscoveryScope } from '@/features/metadata/metadata.store';
import type { Dataset } from '@/types/metadata';
import { getCurrentPage, scopeToUrl } from '@/utils/router';
import { useTranslationCustom } from '@/hooks';
import { BOX_SHADOW } from '@/constants/overviewConstants';
import { RightOutlined } from '@ant-design/icons';
import { useCallback } from 'react';
import SmallChartCardTitle from '@/components/Util/SmallChartCardTitle';

const Dataset = ({
parentProjectID,
dataset,
format,
selected,
}: {
parentProjectID: string;
dataset: Dataset;
format: 'list-item' | 'small-card';
selected?: boolean;
}) => {
const location = useLocation();
const navigate = useNavigate();
const page = getCurrentPage();

const t = useTranslationCustom();

const baseURL = '/' + location.pathname.split('/')[1];

const { identifier, title, description } = dataset;

const scope: DiscoveryScope = { project: parentProjectID, dataset: identifier };
const datasetURL = scopeToUrl(scope, baseURL, `/${page}`);

const onNavigate = useCallback(() => navigate(datasetURL), [navigate, datasetURL]);

if (format === 'list-item') {
return (
<List.Item
className={`select-dataset-item${selected ? ' selected' : ''}`}
key={identifier}
onClick={onNavigate}
style={{ cursor: 'pointer' }}
>
<List.Item.Meta avatar={<Avatar icon={<FaDatabase />} />} title={t(title)} description={t(description)} />
</List.Item>
);
} else if (format === 'small-card') {
return (
<Card
title={<SmallChartCardTitle title={title} />}
size="small"
style={{ ...BOX_SHADOW, height: 200 }}
extra={<Button shape="circle" icon={<RightOutlined />} onClick={onNavigate} />}
>
{description}
</Card>
);
} else {
return <span style={{ color: 'red' }}>UNIMPLEMENTED</span>;
}
};

export default Dataset;
4 changes: 2 additions & 2 deletions src/js/components/Provenance/ProvenanceTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Row } from 'antd';

import { useAppSelector } from '@/hooks';
import { useMetadata } from '@/features/metadata/hooks';
import type { Dataset } from '@/types/metadata';

import DatasetProvenance from './DatasetProvenance';
Expand All @@ -11,7 +11,7 @@ const ProvenanceTab = () => {
projects,
isFetching: loading,
selectedScope: { scope },
} = useAppSelector((state) => state.metadata);
} = useMetadata();

const datasets = useMemo<Dataset[]>(() => {
let filteredProjects = projects;
Expand Down
30 changes: 8 additions & 22 deletions src/js/components/Scope/DatasetScopePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useMemo } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { List, Avatar, Space, Typography } from 'antd';
import { FaDatabase } from 'react-icons/fa';
import { Link, useLocation } from 'react-router-dom';
import { List, Space, Typography } from 'antd';

import type { DiscoveryScope } from '@/features/metadata/metadata.store';
import { useAppSelector, useTranslationCustom, useTranslationDefault } from '@/hooks';
import { useTranslationCustom, useTranslationDefault } from '@/hooks';
import type { Project } from '@/types/metadata';
import { getCurrentPage, scopeEqual, scopeToUrl } from '@/utils/router';
import { getCurrentPage, scopeToUrl } from '@/utils/router';
import Dataset from '@/components/Provenance/Dataset';
import { useSelectedScope } from '@/features/metadata/hooks';

type DatasetScopePickerProps = {
parentProject: Project;
Expand All @@ -17,10 +18,9 @@ const DatasetScopePicker = ({ parentProject }: DatasetScopePickerProps) => {
const t = useTranslationCustom();
const location = useLocation();
const baseURL = '/' + location.pathname.split('/')[1];
const navigate = useNavigate();
const page = getCurrentPage();

const { selectedScope } = useAppSelector((state) => state.metadata);
const selectedScope = useSelectedScope();
const scopeObj = selectedScope.scope;

const showClearDataset = useMemo(
Expand Down Expand Up @@ -52,21 +52,7 @@ const DatasetScopePicker = ({ parentProject }: DatasetScopePickerProps) => {
<List
dataSource={parentProject.datasets}
bordered
renderItem={({ identifier, title, description }) => {
const itemScope = { ...parentProjectScope, dataset: identifier };
const selected = scopeEqual(itemScope, scopeObj); // item scope === dataset scope
const datasetURL = scopeToUrl(itemScope, baseURL, `/${page}`);
return (
<List.Item
className={`select-dataset-item${selected ? ' selected' : ''}`}
key={identifier}
onClick={() => navigate(datasetURL)}
style={{ cursor: 'pointer' }}
>
<List.Item.Meta avatar={<Avatar icon={<FaDatabase />} />} title={t(title)} description={t(description)} />
</List.Item>
);
}}
renderItem={(d) => <Dataset parentProjectID={parentProject.identifier} dataset={d} format="list-item" />}
/>
</Space>
);
Expand Down
7 changes: 5 additions & 2 deletions src/js/components/Scope/ProjectScopePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { type CSSProperties } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Tabs, Button } from 'antd';
import { useAppSelector, useTranslationCustom, useTranslationDefault } from '@/hooks';

import { useLocation, useNavigate } from 'react-router-dom';
import { useMetadata } from '@/features/metadata/hooks';
import { useTranslationCustom, useTranslationDefault } from '@/hooks';

import DatasetScopePicker from './DatasetScopePicker';

const styles: Record<string, CSSProperties> = {
Expand All @@ -21,7 +24,7 @@ const ProjectScopePicker = () => {

const navigate = useNavigate();

const { projects, selectedScope } = useAppSelector((state) => state.metadata);
const { projects, selectedScope } = useMetadata();
const { scope: scopeObj, fixedProject } = selectedScope;

const [selectedProject, setSelectedProject] = useState<string | undefined>(
Expand Down
16 changes: 7 additions & 9 deletions src/js/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useAuthState, useIsAuthenticated, useOpenIdConfig, usePerformAuth, useP
import { RiTranslate } from 'react-icons/ri';
import { ExportOutlined, LinkOutlined, LoginOutlined, LogoutOutlined, ProfileOutlined } from '@ant-design/icons';

import { useAppSelector } from '@/hooks';
import { useSelectedProject, useSelectedScope } from '@/features/metadata/hooks';
import { useSmallScreen } from '@/hooks/useResponsiveContext';
import { scopeToUrl } from '@/utils/router';

Expand All @@ -33,21 +33,19 @@ const SiteHeader = () => {

const { isFetching: openIdConfigFetching } = useOpenIdConfig();
const { isHandingOffCodeForToken } = useAuthState();
const { projects, selectedScope } = useAppSelector((state) => state.metadata);
const { scope: scopeObj } = selectedScope;
const { fixedProject, fixedDataset, scope: scopeObj } = useSelectedScope();
const selectedProject = useSelectedProject();

const scopeSelectionEnabled = !(selectedScope.fixedProject && selectedScope.fixedDataset);
const scopeSelectionEnabled = !(fixedProject && fixedDataset);

const scopeProps = useMemo(
() => ({
projectTitle: projects.find((project) => project.identifier === scopeObj.project)?.title,
projectTitle: selectedProject?.title,
datasetTitle: scopeObj.dataset
? projects
.find((project) => project.identifier === scopeObj.project)
?.datasets.find((dataset) => dataset.identifier === scopeObj.dataset)?.title
? selectedProject?.datasets.find((dataset) => dataset.identifier === scopeObj.dataset)?.title
: null,
}),
[projects, scopeObj]
[selectedProject, scopeObj]
);

const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down
32 changes: 32 additions & 0 deletions src/js/components/Util/SmallChartCardTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { CSSProperties, ReactNode } from 'react';
import { Space, Typography } from 'antd';

type SmallChartCardTitleProps = {
title: ReactNode;
description?: ReactNode;
descriptionStyle?: CSSProperties;
};

const SmallChartCardTitle = ({ title, description, descriptionStyle }: SmallChartCardTitleProps) => (
<Space.Compact
direction="vertical"
style={{ fontWeight: 'normal', padding: description ? '5px 5px' : '10px 5px', maxWidth: '100%' }}
>
<Typography.Text
ellipsis={true}
style={{
fontSize: '20px',
fontWeight: '600',
}}
>
{title}
</Typography.Text>
{description && (
<Typography.Text type="secondary" style={descriptionStyle} ellipsis={{ tooltip: description }}>
{description}
</Typography.Text>
)}
</Space.Compact>
);

export default SmallChartCardTitle;
Loading

0 comments on commit 9e27258

Please sign in to comment.