Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

H-1982: Allow visualizing types in a graph #5172

Merged
merged 7 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/hash-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@ory/client": "1.1.41",
"@ory/integrations": "1.1.4",
"@popperjs/core": "2.11.8",
"@react-sigma/core": "4.0.3",
"@sentry/nextjs": "7.119.0",
"@sentry/react": "7.119.0",
"@svgr/webpack": "8.1.0",
Expand All @@ -69,6 +70,9 @@
"emoji-mart": "5.2.1",
"fractional-indexing": "2.1.0",
"framer-motion": "6.5.1",
"graphology": "0.25.4",
"graphology-layout": "0.6.1",
"graphology-layout-forceatlas2": "0.10.1",
"graphql": "16.9.0",
"iframe-resizer": "4.4.5",
"immer": "9.0.21",
Expand Down Expand Up @@ -96,6 +100,7 @@
"react-beautiful-dnd": "13.1.1",
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-full-screen": "1.1.1",
"react-hook-form": "7.53.0",
"react-markdown": "9.0.1",
"react-responsive-carousel": "3.2.23",
Expand All @@ -106,6 +111,7 @@
"rooks": "7.14.1",
"safe-stable-stringify": "2.5.0",
"setimmediate": "1.0.5",
"sigma": "3.0.0-beta.29",
"signia": "0.1.5",
"signia-react": "0.1.5",
"url-regex-safe": "4.0.0",
Expand Down Expand Up @@ -136,6 +142,7 @@
"@types/uuid": "8.3.4",
"@welldone-software/why-did-you-render": "8.0.3",
"eslint": "8.57.0",
"graphology-types": "0.24.7",
"rimraf": "6.0.1",
"sass": "1.78.0",
"typescript": "5.6.2",
Expand Down
93 changes: 16 additions & 77 deletions apps/hash-frontend/src/pages/shared/entities-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { VersionedUrl } from "@blockprotocol/type-system/slim";
import type { CustomCell, Item, TextCell } from "@glideapps/glide-data-grid";
import { GridCellKind } from "@glideapps/glide-data-grid";
import { EntitiesGraphChart } from "@hashintel/block-design-system";
import { ListRegularIcon } from "@hashintel/design-system";
import type { EntityId } from "@local/hash-graph-types/entity";
import { gridRowHeight } from "@local/hash-isomorphic-utils/data-grid";
import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
Expand All @@ -16,16 +15,9 @@ import {
type Subgraph,
} from "@local/hash-subgraph";
import { extractBaseUrl } from "@local/hash-subgraph/type-system-patch";
import {
Box,
ToggleButton,
toggleButtonClasses,
ToggleButtonGroup,
Tooltip,
useTheme,
} from "@mui/material";
import { Box, useTheme } from "@mui/material";
import { useRouter } from "next/router";
import type { FunctionComponent, ReactNode } from "react";
import type { FunctionComponent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import type { GridProps } from "../../components/grid/grid";
Expand All @@ -40,8 +32,6 @@ import type { CustomIcon } from "../../components/grid/utils/custom-grid-icons";
import type { ColumnFilter } from "../../components/grid/utils/filtering";
import { useEntityTypeEntitiesContext } from "../../shared/entity-type-entities-context";
import { useEntityTypesContextRequired } from "../../shared/entity-types-context/hooks/use-entity-types-context-required";
import { ChartNetworkRegularIcon } from "../../shared/icons/chart-network-regular-icon";
import { GridSolidIcon } from "../../shared/icons/grid-solid-icon";
import { HEADER_HEIGHT } from "../../shared/layout/layout-with-header/page-header";
import type { FilterState } from "../../shared/table-header";
import { TableHeader, tableHeaderHeight } from "../../shared/table-header";
Expand All @@ -59,6 +49,9 @@ import { useEntitiesTable } from "./entities-table/use-entities-table";
import { useGetEntitiesTableAdditionalCsvData } from "./entities-table/use-get-entities-table-additional-csv-data";
import { TypeSlideOverStack } from "./entity-type-page/type-slide-over-stack";
import { generateEntityRootedSubgraph } from "./subgraphs";
import { TableHeaderToggle } from "./table-header-toggle";
import type { TableView } from "./table-views";
import { tableViewIcons } from "./table-views";
import { TOP_CONTEXT_BAR_HEIGHT } from "./top-context-bar";

/**
Expand All @@ -83,16 +76,6 @@ const allFileEntityTypeBaseUrl = allFileEntityTypeOntologyIds.map(
({ entityTypeBaseUrl }) => entityTypeBaseUrl,
);

const entitiesTableViews = ["Table", "Graph", "Grid"] as const;

type EntityTableView = (typeof entitiesTableViews)[number];

const entitiesTableViewIcons: Record<EntityTableView, ReactNode> = {
Table: <ListRegularIcon sx={{ fontSize: 18 }} />,
Graph: <ChartNetworkRegularIcon sx={{ fontSize: 18 }} />,
Grid: <GridSolidIcon sx={{ fontSize: 14 }} />,
};

export const EntitiesTable: FunctionComponent<{
hideEntityTypeVersionColumn?: boolean;
hidePropertiesColumns?: boolean;
Expand Down Expand Up @@ -144,7 +127,7 @@ export const EntitiesTable: FunctionComponent<{

const supportGridView = isDisplayingFilesOnly;

const [view, setView] = useState<EntityTableView>(
const [view, setView] = useState<TableView>(
isDisplayingFilesOnly ? "Grid" : "Table",
);

Expand Down Expand Up @@ -665,65 +648,21 @@ export const EntitiesTable: FunctionComponent<{
currentlyDisplayedRowsRef={currentlyDisplayedRowsRef}
getAdditionalCsvData={getEntitiesTableAdditionalCsvData}
endAdornment={
<ToggleButtonGroup
<TableHeaderToggle
value={view}
exclusive
onChange={(_, updatedView) => {
if (updatedView) {
setView(updatedView);
}
}}
aria-label="view"
size="small"
sx={{
[`.${toggleButtonClasses.root}`]: {
backgroundColor: ({ palette }) => palette.common.white,
"&:not(:last-of-type)": {
borderRightColor: ({ palette }) => palette.gray[20],
borderRightStyle: "solid",
borderRightWidth: 2,
},
"&:hover": {
backgroundColor: ({ palette }) => palette.common.white,
svg: {
color: ({ palette }) => palette.gray[80],
},
},
[`&.${toggleButtonClasses.selected}`]: {
backgroundColor: ({ palette }) => palette.common.white,
svg: {
color: ({ palette }) => palette.gray[90],
},
},
svg: {
transition: ({ transitions }) =>
transitions.create("color"),
color: ({ palette }) => palette.gray[50],
},
},
}}
>
{(
setValue={setView}
options={(
[
"Table",
...(supportGridView ? (["Grid"] as const) : []),
"Graph",
] satisfies EntityTableView[]
).map((viewName) => (
<ToggleButton
key={viewName}
disableRipple
value={viewName}
aria-label={viewName}
>
<Tooltip title={`${viewName} view`} placement="top">
<Box sx={{ lineHeight: 0 }}>
{entitiesTableViewIcons[viewName]}
</Box>
</Tooltip>
</ToggleButton>
))}
</ToggleButtonGroup>
] as const satisfies TableView[]
).map((optionValue) => ({
icon: tableViewIcons[optionValue],
label: `${optionValue} view`,
value: optionValue,
}))}
/>
}
filterState={filterState}
setFilterState={setFilterState}
Expand Down
77 changes: 77 additions & 0 deletions apps/hash-frontend/src/pages/shared/table-header-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Box,
ToggleButton,
toggleButtonClasses,
ToggleButtonGroup,
Tooltip,
} from "@mui/material";
import type { ReactElement } from "react";

type TableHeaderToggleProps<Option extends string> = {
options: {
label: string;
icon: ReactElement;
value: Option;
}[];
setValue: (value: Option) => void;
value: Option;
};

export const TableHeaderToggle = <Option extends string>({
options,
setValue,
value: selectedValue,
}: TableHeaderToggleProps<Option>) => {
return (
<ToggleButtonGroup
value={selectedValue}
exclusive
onChange={(_, updatedValue) => {
if (updatedValue) {
setValue(updatedValue);
}
}}
aria-label="view"
size="small"
sx={{
[`.${toggleButtonClasses.root}`]: {
backgroundColor: ({ palette }) => palette.common.white,
"&:not(:last-of-type)": {
borderRightColor: ({ palette }) => palette.gray[20],
borderRightStyle: "solid",
borderRightWidth: 2,
},
"&:hover": {
backgroundColor: ({ palette }) => palette.common.white,
svg: {
color: ({ palette }) => palette.gray[80],
},
},
[`&.${toggleButtonClasses.selected}`]: {
backgroundColor: ({ palette }) => palette.common.white,
svg: {
color: ({ palette }) => palette.gray[90],
},
},
svg: {
transition: ({ transitions }) => transitions.create("color"),
color: ({ palette }) => palette.gray[50],
},
},
}}
>
{options.map(({ icon, label, value: optionValue }) => (
<ToggleButton
key={optionValue}
disableRipple
value={optionValue}
aria-label={label}
>
<Tooltip title={label} placement="top">
<Box sx={{ lineHeight: 0 }}>{icon}</Box>
</Tooltip>
</ToggleButton>
))}
</ToggleButtonGroup>
);
};
34 changes: 34 additions & 0 deletions apps/hash-frontend/src/pages/shared/table-views.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ListRegularIcon } from "@hashintel/design-system";
import type { SvgIconProps } from "@mui/material";
import type { ReactElement } from "react";

import { ChartNetworkRegularIcon } from "../../shared/icons/chart-network-regular-icon";
import { GridSolidIcon } from "../../shared/icons/grid-solid-icon";

const tableViews = ["Table", "Graph", "Grid"] as const;

export type TableView = (typeof tableViews)[number];

export const tableViewIcons: Record<TableView, ReactElement<SvgIconProps>> = {
Table: (
<ListRegularIcon
sx={{
fontSize: 18,
}}
/>
),
Graph: (
<ChartNetworkRegularIcon
sx={{
fontSize: 18,
}}
/>
),
Grid: (
<GridSolidIcon
sx={{
fontSize: 14,
}}
/>
),
};
81 changes: 81 additions & 0 deletions apps/hash-frontend/src/pages/shared/types-graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import "@react-sigma/core/lib/react-sigma.min.css";

import { MultiDirectedGraph } from "graphology";
import dynamic from "next/dynamic";
import { memo, useState } from "react";

import type { TypesGraphProps } from "./types-graph/graph-loader";
import {
FullScreenContextProvider,
useFullScreen,
} from "./types-graph/shared/full-screen";

const Graph = ({
height,
onTypeClick,
types,
}: Omit<TypesGraphProps, "highlightDepth"> & { height: string | number }) => {
/**
* When a node is hovered or selected, we highlight its neighbors up to this depth.
*
* Not currently exposed as a user setting but could be, thus the state.
*/
const [highlightDepth, _setHighlightDepth] = useState(2);

/**
* WebGL APIs aren't available in the server, so we need to dynamically load any module which uses Sigma/graphology.
*/
const SigmaContainer = dynamic(
import("@react-sigma/core").then((module) => module.SigmaContainer),
{ ssr: false },
);

const TypesGraphLoader = dynamic(
import("./types-graph/graph-loader").then((module) => module.GraphLoader),
{ ssr: false },
);

const FullScreenButton = dynamic(
import("./types-graph/full-screen-button").then(
(module) => module.FullScreenButton,
),
{ ssr: false },
);

const { isFullScreen } = useFullScreen();

return (
<SigmaContainer
graph={MultiDirectedGraph}
style={{ height: isFullScreen ? "100vh" : height }}
>
<FullScreenButton />
<TypesGraphLoader
highlightDepth={highlightDepth}
onTypeClick={onTypeClick}
types={types}
/>
</SigmaContainer>
);
};

export const TypesGraph = memo(
({
height,
onTypeClick,
types,
}: Omit<TypesGraphProps, "highlightDepth"> & { height: string | number }) => {
/**
* WebGL APIs aren't available in the server, so we need to dynamically load any module which uses Sigma/graphology.
*/
if (typeof window !== "undefined") {
return (
<FullScreenContextProvider>
<Graph height={height} onTypeClick={onTypeClick} types={types} />
</FullScreenContextProvider>
);
}

return null;
},
);
Loading
Loading