diff --git a/AGENTS.md b/AGENTS.md index 064d7054d..a50898d82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,7 +106,8 @@ src/ - React 19 + TypeScript - Vite build tool -- TailwindCSS + Radix UI components +- TailwindCSS v4 with CSS config +- Radix UI components - React Router for routing - Cytoscape.js for graph visualization - React Query for data fetching diff --git a/packages/graph-explorer/src/components/Tabular/TableColumnResizer.tsx b/packages/graph-explorer/src/components/Tabular/TableColumnResizer.tsx new file mode 100644 index 000000000..bdf827007 --- /dev/null +++ b/packages/graph-explorer/src/components/Tabular/TableColumnResizer.tsx @@ -0,0 +1,14 @@ +import type { ComponentPropsWithRef } from "react"; +import { cn } from "@/utils"; + +export function TableColumnResizer({ + className, + ...props +}: ComponentPropsWithRef<"div">) { + return ( +
+ ); +} diff --git a/packages/graph-explorer/src/components/Tabular/Tabular.styles.ts b/packages/graph-explorer/src/components/Tabular/Tabular.styles.ts index bdeffafc7..3a72bfb3e 100644 --- a/packages/graph-explorer/src/components/Tabular/Tabular.styles.ts +++ b/packages/graph-explorer/src/components/Tabular/Tabular.styles.ts @@ -1,266 +1,185 @@ import { css } from "@emotion/css"; -import { fade, type ThemeStyleFn } from "@/core"; +import type { ThemeStyleFn } from "@/core"; import baseTheme from "./baseTheme"; -const defaultStyles = - (variant: "bordered" | "noBorders" = "bordered"): ThemeStyleFn => - theme => { - const { palette } = theme; - - return css` - max-width: 100%; - overflow: auto; +const defaultStyles = (): ThemeStyleFn => theme => { + const { palette } = theme; + + return css` + max-width: 100%; + overflow: auto; + display: flex; + flex-direction: column; + width: 100%; + max-height: 100%; + height: 100%; + background: ${palette.background.default}; + color: ${palette.text.primary}; + + .table { + flex-grow: 1; + width: 100%; + position: relative; + display: flex; + flex-direction: column; + } + + .headers { + box-sizing: border-box; + width: fit-content; + min-width: 100%; + background: ${palette.background.contrast}; + color: ${palette.text.secondary}; + border: solid 1px ${palette.border}; + min-height: ${baseTheme.header.minHeight}; + } + + .headers-sticky { + position: sticky; + top: 0; + z-index: 1; + } + + .header { display: flex; flex-direction: column; + padding: ${baseTheme.header.padding}; + border-right: 1px solid ${palette.border}; + background: ${palette.background.contrast}; + color: ${baseTheme.header.color}; + transition: + background 250ms ease-in, + border 250ms ease-in; + } + + .header-label { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + flex-grow: 1; width: 100%; - max-height: 100%; - height: 100%; - background: ${palette.background.default}; - color: ${palette.text.primary}; - - .table { - flex-grow: 1; - width: 100%; - position: relative; - display: flex; - flex-direction: column; - } - - .headers { - box-sizing: border-box; - width: fit-content; - min-width: 100%; - background: ${palette.background.contrast}; - color: ${palette.text.secondary}; - border: solid 1px ${palette.border}; - ${variant === "noBorders" && `border-right: none; border-left: none;`} - min-height: ${baseTheme.header.minHeight}; - - .row:last-child { - border-bottom: none; - } - } - - .headers-sticky { - position: sticky; - top: 0; - z-index: 1; - } - - .row { - box-sizing: border-box; - min-height: ${baseTheme.row.minHeight}; - border-bottom: ${variant === "bordered" - ? `solid 1px ${palette.border}` - : "none"}; - flex-grow: 0 !important; - transition: background-color 250ms ease; - - :first-child { - border-top: solid 1px ${palette.border}; - } - - :hover { - background: ${palette.background.contrast}; - color: ${palette.text.primary}; - } - - :first-child { - border-top: none; - } - } - - .row-grow { - flex-grow: 1 !important; - } - - .row-selectable { - background: ${baseTheme.row.selectable.background}; - color: ${baseTheme.row.selectable.color}; - :hover { - background: ${baseTheme.row.selectable.hover.background}; - color: ${palette.primary.dark}; - cursor: pointer; - } - } - - .row-selected { - background: ${fade(palette.primary.main, 0.25)}; - color: ${baseTheme.row.selected.color}; - } - - .header { - display: flex; - flex-direction: column; - padding: ${baseTheme.header.padding}; - border-right: ${variant === "bordered" - ? `1px solid ${palette.border}` - : "none"}; - background: ${palette.background.contrast}; - color: ${baseTheme.header.color}; - transition: - background 250ms ease-in, - border 250ms ease-in; - } - - .header-label { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - flex-grow: 1; - width: 100%; - font-weight: ${baseTheme.header.label.fontWeight}; - min-height: ${baseTheme.header.label.minHeight}; - margin: ${baseTheme.header.label.margin}; - padding: ${baseTheme.header.label.padding}; - } - - .header-filter { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - flex-grow: 1; - width: 100%; - min-height: ${baseTheme.header.filter.minHeight}; - margin: ${baseTheme.header.filter.margin}; - padding: ${baseTheme.header.filter.padding}; - } - - .header-resizing { - background: ${palette.background.contrastSecondary}; - color: ${baseTheme.header.resizing.color}; - border-right: 1px dashed ${palette.border}; - } - - .header-label-sorter { - display: flex; - justify-content: center; - align-items: center; - transition: transform 250ms ease; - color: ${baseTheme.header.sorter.color}; - opacity: ${baseTheme.header.sorter.opacity}; - } + font-weight: ${baseTheme.header.label.fontWeight}; + min-height: ${baseTheme.header.label.minHeight}; + margin: ${baseTheme.header.label.margin}; + padding: ${baseTheme.header.label.padding}; + } - .cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 0 8px; - overflow: hidden; - position: relative; - transition: border 250ms ease-in; - border-right: ${variant === "bordered" - ? `solid 1px ${palette.border}` - : "none"}; - } - - .cell-content { - width: 100%; - } - - .cell-overflow-ellipsis { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - - .cell-overflow-truncate { - max-width: 100%; - overflow: hidden; - } - - .cell-one-line { - word-break: keep-all; - white-space: nowrap; - } - - .cell-resizing { - background: ${palette.background.contrastSecondary}; - color: ${baseTheme.row.resizing.color}; - border-right: 1px dashed ${palette.border}; - } - - .header-label-align-left, - .cell-align-left { - flex-direction: row; - justify-content: flex-start; - text-align: left; - } - - .header-label-align-right, - .cell-align-right { - flex-direction: row-reverse; - justify-content: flex-start; - text-align: right; - } - - .header-label-sortable { - cursor: pointer; - justify-content: space-between; + .header-filter { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex-grow: 1; + width: 100%; + min-height: ${baseTheme.header.filter.minHeight}; + margin: ${baseTheme.header.filter.margin}; + padding: ${baseTheme.header.filter.padding}; + } - :hover { - > div:first-child { - max-width: calc(100% - 16px); - overflow: hidden; - } + .header-label-sorter { + display: flex; + justify-content: center; + align-items: center; + transition: transform 250ms ease; + color: ${baseTheme.header.sorter.color}; + opacity: ${baseTheme.header.sorter.opacity}; + } + + .cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 8px; + overflow: hidden; + position: relative; + transition: border 250ms ease-in; + border-right: solid 1px ${palette.border}; + } + + .cell-content { + width: 100%; + } - .header-label-sorter { - color: ${baseTheme.header.sorter.hover.color}; - opacity: ${baseTheme.header.sorter.hover.opacity} !important; - } - } - } + .cell-overflow-ellipsis { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } - .header-label-sort-desc, - .header-label-sort-asc { + .cell-overflow-truncate { + max-width: 100%; + overflow: hidden; + } + + .cell-one-line { + word-break: keep-all; + white-space: nowrap; + } + + .header-label-align-left, + .cell-align-left { + flex-direction: row; + justify-content: flex-start; + text-align: left; + } + + .header-label-align-right, + .cell-align-right { + flex-direction: row-reverse; + justify-content: flex-start; + text-align: right; + } + + .header-label-sortable { + cursor: pointer; + justify-content: space-between; + + :hover { > div:first-child { max-width: calc(100% - 16px); overflow: hidden; } - } - .header-overflow-ellipsis { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; + .header-label-sorter { + color: ${baseTheme.header.sorter.hover.color}; + opacity: ${baseTheme.header.sorter.hover.opacity} !important; + } } + } - .header-overflow-truncate { - max-width: 100%; + .header-label-sort-desc, + .header-label-sort-asc { + > div:first-child { + max-width: calc(100% - 16px); overflow: hidden; } + } - .col-resizer, - .cell-resizer { - position: absolute; - top: 0; - right: 0; - width: 8px; - height: 100%; - z-index: 1; - } + .header-overflow-ellipsis { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } - .body { - width: fit-content; - min-width: 100%; - flex-grow: 1; - display: flex; - flex-direction: column; + .header-overflow-truncate { + max-width: 100%; + overflow: hidden; + } - border-top: ${variant === "bordered" - ? `solid 1px ${palette.border}` - : "none"}; - border-right: ${variant === "bordered" - ? `solid 1px ${palette.border}` - : "none"}; - border-left: ${variant === "bordered" - ? `solid 1px ${palette.border}` - : "none"}; - } - `; - }; + .body { + width: fit-content; + min-width: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; + + border-top: solid 1px ${palette.border}; + border-right: solid 1px ${palette.border}; + border-left: solid 1px ${palette.border}; + } + `; +}; export default defaultStyles; diff --git a/packages/graph-explorer/src/components/Tabular/Tabular.tsx b/packages/graph-explorer/src/components/Tabular/Tabular.tsx index d8f189cb3..e94c48a9e 100644 --- a/packages/graph-explorer/src/components/Tabular/Tabular.tsx +++ b/packages/graph-explorer/src/components/Tabular/Tabular.tsx @@ -28,23 +28,9 @@ import TabularRow from "./TabularRow"; import type { TabularOptions } from "./useTabular"; import useTabular from "./useTabular"; -export type TabularVariantType = "bordered" | "noBorders"; - export interface TabularProps extends TabularOptions { className?: string; - /** - * Disables the sticky header. By default, it is pinned to the top of the view - * so that it is visible even when scrolling. - */ - disableStickyHeader?: boolean; - - /** - * Allows row to grow vertically. Each row grows to fit to table available space. - */ - fitRowsVertically?: boolean; - - variant?: TabularVariantType; globalSearch?: string; ref?: React.Ref>; } @@ -59,7 +45,6 @@ export const Tabular = ({ pageSize = 10, onDataFilteredChange, onColumnSortedChange, - variant = "bordered", globalSearch, ref, ...useTabularOptions @@ -80,7 +65,6 @@ export const Tabular = ({ paginationOptions, pageIndex, pageSize, - variant, ...useTabularOptions, }; @@ -115,11 +99,7 @@ export const Tabular = ({ return ( - + ); }; @@ -129,12 +109,9 @@ const TabularContent = ({ className, tableInstance, disablePagination, - disableStickyHeader, - fitRowsVertically, rowSelectionMode, onRowMouseOver, onRowMouseOut, - variant, }: PropsWithChildren< TabularProps & { tableInstance: TableInstance } >) => { @@ -193,7 +170,7 @@ const TabularContent = ({
({ {headerControlsChildren}
({ key={row.id} row={row} tableInstance={tableInstance} - fitRowsVertically={fitRowsVertically} rowSelectionMode={rowSelectionMode} onMouseOver={onRowMouseOver} onMouseOut={onRowMouseOut} @@ -242,7 +216,7 @@ const TabularContent = ({
{footerControlsChildren} {!footerControlsChildren && !disablePagination && ( - + )} diff --git a/packages/graph-explorer/src/components/Tabular/TabularHeader.tsx b/packages/graph-explorer/src/components/Tabular/TabularHeader.tsx index d210be538..37bc429e9 100644 --- a/packages/graph-explorer/src/components/Tabular/TabularHeader.tsx +++ b/packages/graph-explorer/src/components/Tabular/TabularHeader.tsx @@ -3,6 +3,7 @@ import { cn } from "@/utils"; import type { HeaderGroup, TableInstance } from "react-table"; import { ArrowDown } from "@/components/icons"; +import { TableColumnResizer } from "./TableColumnResizer"; const TabularHeader = ({ headerGroup, @@ -29,7 +30,7 @@ const TabularHeader = ({ key={key} style={style} className={cn("header", { - ["header-resizing"]: + ["border-r-primary-main!"]: column.isResizing || state.columnResizing?.isResizingColumn === column.id, })} @@ -70,10 +71,7 @@ const TabularHeader = ({
{column.render("Filter")}
)} {column.canResize && ( -
+ )}
); diff --git a/packages/graph-explorer/src/components/Tabular/TabularRow.tsx b/packages/graph-explorer/src/components/Tabular/TabularRow.tsx index 918f4a67e..c16e4025f 100644 --- a/packages/graph-explorer/src/components/Tabular/TabularRow.tsx +++ b/packages/graph-explorer/src/components/Tabular/TabularRow.tsx @@ -4,15 +4,15 @@ import { type MouseEvent, useEffect, useState } from "react"; import type { Row, TableInstance } from "react-table"; import type { TabularProps } from "./Tabular"; +import { TableColumnResizer } from "./TableColumnResizer"; const TabularRow = ({ - fitRowsVertically, rowSelectionMode, row, tableInstance, onMouseOver, onMouseOut, -}: Pick, "fitRowsVertically" | "rowSelectionMode"> & { +}: Pick, "rowSelectionMode"> & { tableInstance: TableInstance; row: Row; onMouseOver?(event: MouseEvent, row: Row): void; @@ -44,10 +44,9 @@ const TabularRow = ({ return (
rowSelectionMode === "row" && selectable && row.toggleRowSelected() @@ -64,13 +63,13 @@ const TabularRow = ({ key={key} {...cellProps} className={cn("cell", `cell-align-${cell.column.align || "left"}`, { - ["cell-resizing"]: + ["border-r-primary-main!"]: cell.column.isResizing || state.columnResizing?.isResizingColumn === cell.column.id, })} >
({ {cell.render("Cell")}
{cell.column.canResize && ( -
)}
diff --git a/packages/graph-explorer/src/components/Tabular/baseTheme.ts b/packages/graph-explorer/src/components/Tabular/baseTheme.ts index ed349aeeb..42393ba7b 100644 --- a/packages/graph-explorer/src/components/Tabular/baseTheme.ts +++ b/packages/graph-explorer/src/components/Tabular/baseTheme.ts @@ -39,33 +39,6 @@ const baseTheme = { padding: "4px 8px", }, }, - row: { - background: "inherit", - color: "inherit", - border: "solid 1px #d3d3d3", - minHeight: "32px", - hover: { - background: "rgba(238,238,238,0.5)", - color: "inherit", - }, - selectable: { - background: "inherit", - color: "inherit", - hover: { - background: "rgba(18, 142, 229, 0.5)", - color: "inherit", - }, - }, - selected: { - background: "rgba(18, 142, 229, 0.25)", - color: "inherit", - }, - resizing: { - background: "inherit", - color: "inherit", - border: "dashed 1px #d3d3d3", - }, - }, footer: { controls: { background: "#fff", diff --git a/packages/graph-explorer/src/components/Tabular/controls/PlaceholderControl.tsx b/packages/graph-explorer/src/components/Tabular/controls/PlaceholderControl.tsx deleted file mode 100644 index 7fe59574e..000000000 --- a/packages/graph-explorer/src/components/Tabular/controls/PlaceholderControl.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { css } from "@emotion/css"; - -import type { PropsWithChildren } from "react"; - -const defaultStyles = () => css` - font-style: italic; - font-size: 1.3rem; - opacity: 0.7; - color: var(--palette-primary-main); -`; - -export const PlaceholderControl = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -export default PlaceholderControl; diff --git a/packages/graph-explorer/src/components/Tabular/controls/TabularFooterControls.tsx b/packages/graph-explorer/src/components/Tabular/controls/TabularFooterControls.tsx index e180159a3..1b4240627 100644 --- a/packages/graph-explorer/src/components/Tabular/controls/TabularFooterControls.tsx +++ b/packages/graph-explorer/src/components/Tabular/controls/TabularFooterControls.tsx @@ -2,14 +2,12 @@ import { css } from "@emotion/css"; import { cn } from "@/utils"; import type { FC, PropsWithChildren } from "react"; import { useWithTheme } from "@/core"; -import type { TabularVariantType } from "../Tabular"; import type { ThemeStyleFn } from "@/core/ThemeProvider"; import baseTheme from "../baseTheme"; export type TabularFooterControlsProps = PropsWithChildren<{ className?: string; - variant?: TabularVariantType; /** * Disables the sticky footer controls. By default, it is pinned to the bottom of the view * so that it is visible even when scrolling. @@ -17,53 +15,47 @@ export type TabularFooterControlsProps = PropsWithChildren<{ disableSticky?: boolean; }>; -const defaultStyles = - (variant?: TabularVariantType): ThemeStyleFn => - theme => { - const { palette } = theme; +const defaultStyles = (): ThemeStyleFn => theme => { + const { palette } = theme; - return css` - &.footer-controls { - position: sticky; - left: 0; - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - background: ${palette.background.default}; - color: ${palette.text.primary}; - padding: ${baseTheme.footer.controls.padding}; - border: solid 1px ${palette.border}; + return css` + &.footer-controls { + position: sticky; + left: 0; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + background: ${palette.background.default}; + color: ${palette.text.primary}; + padding: ${baseTheme.footer.controls.padding}; + border: solid 1px ${palette.border}; - ${variant === "noBorders" && - `border-right: none; border-left: none; border-bottom: none;`} - - > * { - margin: 0 4px; - } + > * { + margin: 0 4px; } + } - &.footer-controls-sticky { - z-index: 3; - // -1px fixes the border - ${variant === "noBorders" ? "bottom: 0;" : "bottom: -1px;"} - } - `; - }; + &.footer-controls-sticky { + z-index: 3; + // -1px fixes the border + bottom: -1px; + } + `; +}; const TabularFooterControls: FC = ({ children, className, disableSticky, - variant, }) => { const styleWithTheme = useWithTheme(); return (
; -const defaultStyles = - (variant?: TabularVariantType): ThemeStyleFn => - () => css` - &.header-controls { - position: sticky; - left: 0; - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - width: 100%; - min-height: 36px; - background: ${cssVar( - "--tabular-header-controls-background", - "--tabular-header-background", - "--palette-background-contrast" - )}; - color: ${cssVar( - "--tabular-header-controls-color", - "--tabular-header-color" - )}; - padding: ${cssVar("--tabular-header-controls-padding")}; - ${variant !== "noBorders" && - ` +const defaultStyles = (): ThemeStyleFn => () => css` + &.header-controls { + position: sticky; + left: 0; + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + width: 100%; + min-height: 36px; + background: ${cssVar( + "--tabular-header-controls-background", + "--tabular-header-background", + "--palette-background-contrast" + )}; + color: ${cssVar( + "--tabular-header-controls-color", + "--tabular-header-color" + )}; + padding: ${cssVar("--tabular-header-controls-padding")}; + border-top: ${cssVar( "--tabular-header-controls-border", "--tabular-header-controls-border", @@ -59,24 +54,22 @@ const defaultStyles = "--tabular-border", "solid 1px var(--palette-border)" )}; - `} - > * { - margin: 0 4px; - } + > * { + margin: 0 4px; } + } - &.header-controls-sticky { - z-index: 2; - top: 0; - } - `; + &.header-controls-sticky { + z-index: 2; + top: 0; + } +`; const TabularHeaderControls: FC = ({ children, className, disableSticky, - variant, }) => { const { headerControlsRef, setHeaderControlsPosition } = useTabularControl(); useEffect(() => { @@ -87,7 +80,7 @@ const TabularHeaderControls: FC = ({
{data.length === 0 && ( - - {t("entities-tabular.edges-placeholder")} - + + + No Data + + {t("entities-tabular.edges-placeholder")} + + + )} diff --git a/packages/graph-explorer/src/modules/EntitiesTabular/components/NodesTabular.tsx b/packages/graph-explorer/src/modules/EntitiesTabular/components/NodesTabular.tsx index 834c04777..f9bf9f87e 100644 --- a/packages/graph-explorer/src/modules/EntitiesTabular/components/NodesTabular.tsx +++ b/packages/graph-explorer/src/modules/EntitiesTabular/components/NodesTabular.tsx @@ -1,11 +1,15 @@ import difference from "lodash/difference"; -import { NonVisibleIcon, VisibleIcon } from "@/components"; +import { + EmptyState, + EmptyStateContent, + EmptyStateDescription, + EmptyStateTitle, + NonVisibleIcon, + VisibleIcon, +} from "@/components"; import type { ColumnDefinition, TabularInstance } from "@/components/Tabular"; import { makeIconToggleCell } from "@/components/Tabular/builders"; -import { - PlaceholderControl, - TabularEmptyBodyControls, -} from "@/components/Tabular/controls"; +import { TabularEmptyBodyControls } from "@/components/Tabular/controls"; import Tabular from "@/components/Tabular/Tabular"; import { type DisplayVertex, @@ -156,9 +160,14 @@ function NodesTabular({ ref }: NodesTabularProps) { > {data.length === 0 && ( - - {t("entities-tabular.nodes-placeholder")} - + + + No Data + + {t("entities-tabular.nodes-placeholder")} + + + )} diff --git a/packages/graph-explorer/src/routes/DataExplorer/DataExplorer.tsx b/packages/graph-explorer/src/routes/DataExplorer/DataExplorer.tsx index 70de8e5e6..30365acef 100644 --- a/packages/graph-explorer/src/routes/DataExplorer/DataExplorer.tsx +++ b/packages/graph-explorer/src/routes/DataExplorer/DataExplorer.tsx @@ -15,6 +15,10 @@ import { buttonStyles, CheckIcon, ChevronLeftIcon, + EmptyState, + EmptyStateContent, + EmptyStateDescription, + EmptyStateTitle, Panel, PanelError, PanelHeader, @@ -27,7 +31,6 @@ import { ExplorerIcon } from "@/components/icons"; import { type ColumnDefinition, PaginationControl, - PlaceholderControl, TabularEmptyBodyControls, TabularFooterControls, type TabularInstance, @@ -142,9 +145,14 @@ function DataExplorerContent({ vertexType }: ConnectionsProps) { ) : null} {query.data?.vertices.length === 0 && ( - - {`No nodes found for "${displayTypeConfig.displayLabel}"`} - + + + No Data + + {`No nodes found for "${displayTypeConfig.displayLabel}"`} + + + )}