Skip to content

Commit 233706e

Browse files
Move graph settings into settings page
limit zoom in default fitview graph show risk score by default now
1 parent c2707cd commit 233706e

17 files changed

Lines changed: 222 additions & 126 deletions

File tree

packages/app-builder/src/components/CaseManager/MainLinksGraph.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { CustomerGraphProvider } from '@app-builder/components/Graph/CustomerGraphContext';
22
import { graphEdgeTypes, graphNodeTypes } from '@app-builder/components/Graph/GraphComponents';
3-
import { GraphMeasuredLayout, useLaidOutGraph } from '@app-builder/components/Graph/use-laid-out-graph';
3+
import {
4+
GraphMeasuredLayout,
5+
graphFitViewOptions,
6+
useLaidOutGraph,
7+
} from '@app-builder/components/Graph/use-laid-out-graph';
48
import { type DataModel } from '@app-builder/models/data-model';
59
import { type GraphData } from '@app-builder/models/graph';
610
import { useGenerateGraphQuery } from '@app-builder/queries/graph/generate-graph';
@@ -71,6 +75,7 @@ function MainLinksGraphCanvas({ data, dataModel }: { data: GraphData; dataModel:
7175
onNodesChange={onNodesChange}
7276
onEdgesChange={onEdgesChange}
7377
fitView
78+
fitViewOptions={graphFitViewOptions}
7479
maxZoom={5}
7580
minZoom={0.1}
7681
nodesDraggable={false}

packages/app-builder/src/components/Graph/CustomerGraphContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { type GraphLayoutMode } from './graph-layout';
66
import { EMPTY_RELATION_FILTER, type RelationFilter, withAvailableLabels, withLabelToggled } from './relation-filter';
77

88
/** Branch sizes a subtree must exceed to collapse into a cluster chip. `0` disables clustering. */
9-
export const CLUSTER_THRESHOLD_OPTIONS = [0, 2, 5, 7, 10, 15, 30, 50] as const;
9+
export const CLUSTER_THRESHOLD_OPTIONS = [0, 1, 2, 3, 5, 8, 13, 21, 34] as const;
1010
export type ClusterThreshold = (typeof CLUSTER_THRESHOLD_OPTIONS)[number];
11-
export const DEFAULT_CLUSTER_THRESHOLD: ClusterThreshold = 10;
11+
export const DEFAULT_CLUSTER_THRESHOLD: ClusterThreshold = 8;
1212

1313
/**
1414
* The node backing the settings panel's detail card. `persons` are the selection's

packages/app-builder/src/components/Graph/GraphImpl.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { graphEdgeTypes, graphNodeTypes } from './GraphComponents';
1111
import { graphI18n } from './graph-i18n';
1212
import { type GraphObjectRef, nodeKey, parseNodeKey } from './graph-keys';
1313
import { type GraphRfNode } from './graph-rf-types';
14-
import { applyVisibilityFilters, GraphMeasuredLayout, useLaidOutGraph } from './use-laid-out-graph';
14+
import {
15+
applyVisibilityFilters,
16+
GraphMeasuredLayout,
17+
graphFitViewOptions,
18+
useLaidOutGraph,
19+
} from './use-laid-out-graph';
1520
import '@xyflow/react/dist/style.css';
1621

1722
export type GraphImplProps = {
@@ -166,13 +171,14 @@ export function GraphImpl({ data, dataModel }: GraphImplProps) {
166171
onNodeMouseEnter={onNodeMouseEnter}
167172
onNodeMouseLeave={onNodeMouseLeave}
168173
fitView
174+
fitViewOptions={graphFitViewOptions}
169175
maxZoom={5}
170176
minZoom={0.1}
171177
proOptions={{ hideAttribution: true }}
172178
colorMode={theme.theme}
173179
>
174180
<GraphMeasuredLayout layoutElements={autoLayoutElements} />
175-
<Controls position="bottom-left" className="z-10">
181+
<Controls position="bottom-left" className="z-10" fitViewOptions={graphFitViewOptions}>
176182
<AutoLayoutControlButton layoutElements={autoLayoutElements} />
177183
<ControlButton
178184
onClick={() => setShowEdgeLabels(!showEdgeLabels)}

packages/app-builder/src/components/Graph/GraphRelationsSettings.tsx

Lines changed: 115 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import { useListGraphRelationsQuery } from '@app-builder/queries/graph/list-rela
1212
import { createGraphRelationPayloadSchema } from '@app-builder/schemas/graph';
1313
import { getFieldErrors, handleSubmit } from '@app-builder/utils/form';
1414
import { useForm } from '@tanstack/react-form';
15+
import { createColumnHelper, getCoreRowModel } from '@tanstack/react-table';
1516
import { useMemo, useState } from 'react';
1617
import { useTranslation } from 'react-i18next';
17-
import { Button, Input, Modal } from 'ui-design-system';
18+
import { Button, Input, Modal, Table, useTable } from 'ui-design-system';
1819
import { Icon } from 'ui-icons';
1920
import { z } from 'zod/v4';
2021
import { GraphOptionSelect } from './GraphOptionSelect';
21-
import { useGraphSession } from './GraphSessionContext';
2222
import { GraphTabSwitch, tabSwitchOptions } from './GraphTabSwitch';
2323
import { graphI18n } from './graph-i18n';
2424

@@ -64,6 +64,8 @@ function joinableFields(rightTable: TableModel | undefined, leftField: DataModel
6464
/** The relations sharing one label, which is what the settings UI calls a "setting". */
6565
type RelationGroup = { label: string; relations: GraphRelation[] };
6666

67+
const relationGroupColumnHelper = createColumnHelper<RelationGroup>();
68+
6769
function groupRelationsByLabel(relations: GraphRelation[]): RelationGroup[] {
6870
const groups = new Map<string, GraphRelation[]>();
6971
for (const relation of relations) {
@@ -158,7 +160,7 @@ function TableFieldSelect({
158160

159161
function RelationEndpoints({ relation }: { relation: GraphRelation }) {
160162
return (
161-
<div className="text-grey-secondary flex min-w-0 items-center gap-xs text-xs">
163+
<div className="text-grey-secondary flex min-w-0 items-center gap-xs text-sm">
162164
<span className="truncate">
163165
{relation.leftType}.{relation.leftField}
164166
</span>
@@ -302,7 +304,6 @@ function RelationSettingPanel({
302304
relations: GraphRelation[];
303305
}) {
304306
const { t } = useTranslation(graphI18n);
305-
const { reloadGraph } = useGraphSession();
306307
const createMutation = useCreateGraphRelationMutation();
307308
const deleteMutation = useDeleteGraphRelationMutation();
308309
const [scope, setScope] = useState<RelationScope>('same-table');
@@ -316,7 +317,6 @@ function RelationSettingPanel({
316317
{ ...value, label },
317318
{
318319
onSuccess: () => {
319-
reloadGraph();
320320
formApi.setFieldValue('leftField', '');
321321
formApi.setFieldValue('rightField', '');
322322
},
@@ -452,7 +452,7 @@ function RelationSettingPanel({
452452
mode="icon"
453453
aria-label={t('graph:settings.delete_relation')}
454454
disabled={deleteMutation.isPending}
455-
onClick={() => deleteMutation.mutate({ relationId: relation.id }, { onSuccess: reloadGraph })}
455+
onClick={() => deleteMutation.mutate({ relationId: relation.id })}
456456
>
457457
<Icon icon="delete" className="size-4" />
458458
</Button>
@@ -474,7 +474,6 @@ function RelationSettingPanel({
474474
export function GraphRelationsSettings({ dataModel }: { dataModel: DataModel }) {
475475
const { t } = useTranslation(graphI18n);
476476
const relationsQuery = useListGraphRelationsQuery();
477-
const { reloadGraph } = useGraphSession();
478477
const deleteRelationsMutation = useDeleteGraphRelationsMutation();
479478

480479
const [createModalOpen, setCreateModalOpen] = useState(false);
@@ -491,13 +490,86 @@ export function GraphRelationsSettings({ dataModel }: { dataModel: DataModel })
491490
return relationsQuery.data.filter((relation) => relation.label === panelLabel);
492491
}, [panelLabel, relationsQuery.data]);
493492

493+
const columns = useMemo(
494+
() => [
495+
relationGroupColumnHelper.accessor((row) => row.label, {
496+
id: 'label',
497+
header: t('graph:settings.create_label.field'),
498+
size: 200,
499+
}),
500+
relationGroupColumnHelper.accessor((row) => row.relations, {
501+
id: 'relations',
502+
header: t('graph:settings.relations'),
503+
size: 400,
504+
cell: ({ getValue }) => {
505+
const relations = getValue();
506+
if (relations.length === 0) return null;
507+
508+
return (
509+
<ul className="flex flex-col gap-xs py-xs">
510+
{relations.map((relation) => (
511+
<li key={relation.id}>
512+
<RelationEndpoints relation={relation} />
513+
</li>
514+
))}
515+
</ul>
516+
);
517+
},
518+
}),
519+
relationGroupColumnHelper.display({
520+
id: 'actions',
521+
size: 80,
522+
cell: ({ cell }) => {
523+
const group = cell.row.original;
524+
const buttonClass = 'group-hover:visible invisible';
525+
return (
526+
<div className="flex gap-sm">
527+
<div className={buttonClass}>
528+
<Button
529+
type="button"
530+
variant="secondary"
531+
appearance="stroked"
532+
mode="icon"
533+
aria-label={t('common:edit')}
534+
onClick={() => setPanelLabel(group.label)}
535+
>
536+
<Icon icon="edit-square" className="size-6 shrink-0" />
537+
</Button>
538+
</div>
539+
<div className={buttonClass}>
540+
<Button
541+
type="button"
542+
variant="secondary"
543+
appearance="stroked"
544+
mode="icon"
545+
aria-label={t('graph:settings.delete_group', { label: group.label })}
546+
onClick={() => setDeleteTarget(group)}
547+
>
548+
<Icon icon="delete" className="size-6 shrink-0" />
549+
</Button>
550+
</div>
551+
</div>
552+
);
553+
},
554+
}),
555+
],
556+
[t],
557+
);
558+
559+
const { table, getBodyProps, rows, getContainerProps } = useTable({
560+
data: groups,
561+
columns,
562+
columnResizeMode: 'onChange',
563+
getCoreRowModel: getCoreRowModel(),
564+
enableSorting: false,
565+
});
566+
494567
// On failure the modal stays open, so the user can retry what is left of the setting.
495568
const onDeleteSetting = (group: RelationGroup) => {
496569
deleteRelationsMutation.mutate(
497570
group.relations.map((relation) => relation.id),
498571
{
499572
onSuccess: () => {
500-
reloadGraph();
501573
if (panelLabel === group.label) setPanelLabel(null);
502574
setDeleteTarget(null);
503575
},
@@ -506,66 +578,42 @@ export function GraphRelationsSettings({ dataModel }: { dataModel: DataModel })
506578
};
507579

508580
return (
509-
<div className="flex min-h-0 flex-1 flex-col gap-lg overflow-y-auto">
510-
<section className="flex flex-col gap-md">
511-
<div className="flex items-center justify-between gap-md">
512-
<h2 className="text-grey-primary text-sm font-semibold">{t('graph:settings.configured_relations')}</h2>
513-
<Button variant="primary" onClick={() => setCreateModalOpen(true)}>
514-
<Icon icon="plus" className="size-4" />
581+
<>
582+
<CollapsiblePaper.Container>
583+
<CollapsiblePaper.Title>
584+
<span className="flex-1">{t('graph:settings.configured_relations')}</span>
585+
<Button
586+
onClick={(event) => {
587+
event.stopPropagation();
588+
setCreateModalOpen(true);
589+
}}
590+
>
591+
<Icon icon="plus" className="size-5" />
515592
{t('common:create')}
516593
</Button>
517-
</div>
518-
519-
{relationsQuery.isPending ? (
520-
<p className="text-grey-secondary text-sm">{t('graph:settings.loading')}</p>
521-
) : relationsQuery.isError ? (
522-
<div className="flex items-center gap-sm">
523-
<p className="text-grey-secondary text-sm">{t('graph:settings.load_error')}</p>
524-
<Button variant="secondary" onClick={() => relationsQuery.refetch()}>
525-
{t('common:retry')}
526-
</Button>
527-
</div>
528-
) : groups.length === 0 ? (
529-
<p className="text-grey-secondary text-sm">{t('graph:settings.empty')}</p>
530-
) : (
531-
<div className="flex flex-col gap-md">
532-
{groups.map((group) => (
533-
<CollapsiblePaper.Container key={group.label} defaultOpen={false}>
534-
<CollapsiblePaper.Title size="small" iconPosition="left">
535-
<span className="text-grey-primary min-w-0 flex-1 truncate text-sm font-medium">{group.label}</span>
536-
<div
537-
className="flex shrink-0 items-center gap-sm"
538-
onClick={(event) => event.stopPropagation()}
539-
onKeyDown={(event) => event.stopPropagation()}
540-
>
541-
<Button variant="secondary" appearance="stroked" onClick={() => setPanelLabel(group.label)}>
542-
{t('common:edit')}
543-
</Button>
544-
<Button
545-
variant="destructive"
546-
appearance="stroked"
547-
mode="icon"
548-
aria-label={t('graph:settings.delete_group', { label: group.label })}
549-
onClick={() => setDeleteTarget(group)}
550-
>
551-
<Icon icon="delete" className="size-4" />
552-
</Button>
553-
</div>
554-
</CollapsiblePaper.Title>
555-
<CollapsiblePaper.Content>
556-
<ul className="divide-grey-border flex flex-col divide-y">
557-
{group.relations.map((relation) => (
558-
<li key={relation.id} className="py-sm first:pt-0 last:pb-0">
559-
<RelationEndpoints relation={relation} />
560-
</li>
561-
))}
562-
</ul>
563-
</CollapsiblePaper.Content>
564-
</CollapsiblePaper.Container>
565-
))}
566-
</div>
567-
)}
568-
</section>
594+
</CollapsiblePaper.Title>
595+
<CollapsiblePaper.Content>
596+
{relationsQuery.isPending ? (
597+
<p className="text-grey-secondary text-sm">{t('graph:settings.loading')}</p>
598+
) : relationsQuery.isError ? (
599+
<div className="flex items-center gap-sm">
600+
<p className="text-grey-secondary text-sm">{t('graph:settings.load_error')}</p>
601+
<Button variant="secondary" onClick={() => relationsQuery.refetch()}>
602+
{t('common:retry')}
603+
</Button>
604+
</div>
605+
) : (
606+
<Table.Container {...getContainerProps()} className="max-h-96">
607+
<Table.Header headerGroups={table.getHeaderGroups()} />
608+
<Table.Body {...getBodyProps()}>
609+
{rows.map((row) => (
610+
<Table.Row key={row.id} className="hover:bg-surface-row-hover group" row={row} />
611+
))}
612+
</Table.Body>
613+
</Table.Container>
614+
)}
615+
</CollapsiblePaper.Content>
616+
</CollapsiblePaper.Container>
569617

570618
<CreateLabelModal open={createModalOpen} onOpenChange={setCreateModalOpen} onSubmitLabel={setPanelLabel} />
571619

@@ -592,6 +640,6 @@ export function GraphRelationsSettings({ dataModel }: { dataModel: DataModel })
592640
relations={panelRelations}
593641
/>
594642
) : null}
595-
</div>
643+
</>
596644
);
597645
}

packages/app-builder/src/components/Graph/GraphSessionContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function GraphSessionProvider({
5757
const [loadedRecord, setLoadedRecord] = useState<GraphRecordRef | null>(seed);
5858
const [showPersons, setShowPersons] = useState(true);
5959
const [showCompanies, setShowCompanies] = useState(true);
60-
const [showRiskScore, setShowRiskScore] = useState(false);
60+
const [showRiskScore, setShowRiskScore] = useState(true);
6161
const [showTags, setShowTags] = useState(false);
6262
const [showEdgeLabels, setShowEdgeLabels] = useState(false);
6363
const [clusterThreshold, setClusterThreshold] = useState<ClusterThreshold>(DEFAULT_CLUSTER_THRESHOLD);

packages/app-builder/src/components/Graph/use-laid-out-graph.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import { type GraphRfEdge, type GraphRfNode } from './graph-rf-types';
1111
import { allowsPivot } from './relation-filter';
1212
import { toFlatFlowElements } from './utils';
1313

14+
/** Caps auto-fit at native node size so sparse graphs don't fill the pane. */
15+
export const graphFitViewOptions = { maxZoom: 1 } as const;
16+
1417
/** Re-run layout once React Flow has measured node sizes (must be a ReactFlow child). */
1518
export function GraphMeasuredLayout({
1619
layoutElements,
1720
}: {
1821
layoutElements: (nodes: GraphRfNode[], edges: GraphRfEdge[]) => { nodes: GraphRfNode[]; edges: GraphRfEdge[] };
1922
}) {
20-
useLayoutInitializedNodes({ mode: 'onNodesInitialized', layoutElements });
23+
useLayoutInitializedNodes({ mode: 'onNodesInitialized', layoutElements, fitViewOptions: graphFitViewOptions });
2124
return null;
2225
}
2326

0 commit comments

Comments
 (0)