Skip to content

Commit 5e6150e

Browse files
committed
[layout] design when layout is running
While a layout is running, clickin on a layout item on the menu stop the running layout and set this layout as the latest (for buttons on the graphs). Now it's possible to change the layout parameter when its is already running. A restart is called. iOn the menu, display a spinner next the algo that is running.
1 parent 64adc3c commit 5e6150e

File tree

11 files changed

+125
-53
lines changed

11 files changed

+125
-53
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"lerna": "^8.2.0",
4141
"playwright": "^1.50.1",
4242
"prettier": "^3.4.2",
43+
"react-what-changed": "^1.1.2",
4344
"rimraf": "^6.0.1",
4445
"typescript": "^5.7.3",
4546
"typescript-eslint": "^8.22.0",

packages/gephi-lite/src/components/SideMenu.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IconType } from "react-icons";
77
import TetherComponent from "react-tether";
88

99
import { CaretDownIcon, CaretRightIcon } from "./common-icons";
10+
import { Spinner } from "./Loader";
1011

1112
type MenuCommon<T = unknown> = {
1213
id: string;
@@ -22,17 +23,18 @@ type MenuCommon<T = unknown> = {
2223
}
2324
) &
2425
T;
25-
type MenuButton<T> = MenuCommon<T> & { type?: "button" };
26+
type MenuButton<T> = MenuCommon<T> & { type?: "button" , isRunning?:boolean};
2627
type MenuText<T> = MenuCommon<T> & { type: "text"; className?: string };
2728
type MenuSimpleItem<T> = MenuButton<T> | MenuText<T>;
28-
type MenuSection<T> = MenuSimpleItem<T> & { children: MenuSimpleItem<T>[] };
29+
export type MenuSection<T> = MenuSimpleItem<T> & { children: MenuSimpleItem<T>[] };
2930
export type MenuItem<T = unknown> = MenuSimpleItem<T> | MenuSection<T>;
3031

31-
const ItemMenuInner: FC<{ item: MenuItem; isOpened?: boolean; isSelected?: boolean }> = ({
32+
const ItemMenuInner: FC<{ item: MenuItem; isOpened?: boolean; isSelected?: boolean, isLoading? :boolean }> = ({
3233
item,
3334
isOpened,
3435
isSelected,
35-
}) => {
36+
isLoading
37+
}) =>{
3638
const { t } = useTranslation();
3739
return (
3840
<div className="d-flex align-items-center w-100">
@@ -46,6 +48,7 @@ const ItemMenuInner: FC<{ item: MenuItem; isOpened?: boolean; isSelected?: boole
4648
<span className="side-menu-item">{item.capitalize ? capitalize(t(item.i18nKey)) : t(item.i18nKey)}</span>
4749
</>
4850
)}
51+
{isLoading && (<Spinner className="spinner-border-sm ms-2"/>)}
4952
{isOpened !== undefined && <span>{isOpened ? <CaretDownIcon /> : <CaretRightIcon />}</span>}
5053
</div>
5154
);
@@ -69,7 +72,7 @@ function SimpleItem<T = unknown>({
6972
className={cx("gl-btn w-100 text-start", selected === item.id && "gl-btn-fill")}
7073
onClick={() => onSelectedChange(item)}
7174
>
72-
<ItemMenuInner item={item} isSelected={selected === item.id} />
75+
<ItemMenuInner item={item} isSelected={selected === item.id} isLoading={item.isRunning}/>
7376
</button>
7477
);
7578
}

packages/gephi-lite/src/components/forms/LayoutQualityForm.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { FC } from "react";
1+
import { FC, useEffect } from "react";
22
import { useTranslation } from "react-i18next";
33

4-
import { useLayoutActions, useLayoutState } from "../../core/context/dataContexts";
4+
import { useLayoutActions, useLayoutState, useSessionActions } from "../../core/context/dataContexts";
55

66
export const LayoutQualityForm: FC = () => {
77
const { t } = useTranslation();
8-
const { quality } = useLayoutState();
9-
const { setQuality } = useLayoutActions();
8+
const { setLastLayout } = useSessionActions();
9+
const layoutState = useLayoutState();
10+
const { setQuality, stopLayout } = useLayoutActions();
11+
12+
useEffect(() => {
13+
setLastLayout("layout-quality");
14+
if (layoutState.type === "running") stopLayout();
15+
}, [layoutState, stopLayout, setLastLayout]);
1016

1117
return (
1218
<div className="panel-body">
@@ -24,9 +30,9 @@ export const LayoutQualityForm: FC = () => {
2430
<input
2531
className="form-check-input"
2632
id="qualityEnabled"
27-
checked={quality.enabled}
33+
checked={layoutState.quality.enabled}
2834
type="checkbox"
29-
onChange={(e) => setQuality({ ...quality, enabled: e.target.checked })}
35+
onChange={(e) => setQuality({ ...layoutState.quality, enabled: e.target.checked })}
3036
/>
3137
<label htmlFor="qualityEnabled" className="form-check-label">
3238
{t("layouts.quality.enable")}
@@ -36,9 +42,9 @@ export const LayoutQualityForm: FC = () => {
3642
<input
3743
className="form-check-input"
3844
id="qualityGrid"
39-
checked={quality.showGrid}
45+
checked={layoutState.quality.showGrid}
4046
type="checkbox"
41-
onChange={(e) => setQuality({ ...quality, showGrid: e.target.checked })}
47+
onChange={(e) => setQuality({ ...layoutState.quality, showGrid: e.target.checked })}
4248
/>
4349
<label htmlFor="qualityGrid" className="form-check-label">
4450
{t("layouts.quality.showGrid")}

packages/gephi-lite/src/core/layouts/index.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { localStorage } from "../../utils/storage";
77
import { EVENTS, emitter } from "../context/eventsContext";
88
import { graphDatasetActions, graphDatasetAtom, sigmaGraphAtom } from "../graph";
99
import { dataGraphToFullGraph } from "../graph/utils";
10-
import { sessionActions, sessionAtom } from "../session";
10+
import { sessionAtom } from "../session";
1111
import { resetCamera } from "../sigma";
1212
import { LAYOUTS } from "./collection";
1313
import { LayoutMapping, LayoutQuality, LayoutState } from "./types";
@@ -62,19 +62,16 @@ export const stopLayout = asyncAction(async (isForRestart = false) => {
6262
export const startLayout = asyncAction(async (id: string, params: unknown, isForRestart = false) => {
6363
// Stop the previous algo (the "if needed" is done in the function itself)
6464
await stopLayout(isForRestart);
65-
65+
6666
const { setNodePositions } = graphDatasetActions;
67-
const { setLastLayoutUsed } = sessionActions;
6867

6968
// search the layout
7069
const layout = LAYOUTS.find((l) => l.id === id);
7170

7271
if (layout) {
73-
if (!isForRestart) setLastLayoutUsed(layout.id);
74-
7572
// Sync layout
7673
if (layout.type === "sync") {
77-
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id }));
74+
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id, supervisor: undefined }));
7875

7976
// generate positions
8077
const dataset = graphDatasetAtom.get();
@@ -104,8 +101,8 @@ export const startLayout = asyncAction(async (id: string, params: unknown, isFor
104101
export const restartLastLayout = asyncAction(async () => {
105102
// Get the algo and its parameters
106103
const session = sessionAtom.get();
107-
if (session.lastLayoutUsed) {
108-
const layoutId = session.lastLayoutUsed;
104+
if (session.lastLayout) {
105+
const layoutId = session.lastLayout;
109106
const layout = LAYOUTS.find((e) => e.id === layoutId);
110107
const params = session.layoutsParameters[layoutId] || {};
111108
if (layout) {

packages/gephi-lite/src/core/session/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ export const reset: Producer<Session, []> = () => {
1818
return () => getEmptySession();
1919
};
2020

21-
const setLastLayoutUsed: Producer<Session, [Session["lastLayoutUsed"]]> = (lastLayoutUsed) => {
21+
const setLastLayout: Producer<Session, [Session["lastLayout"]]> = (layoutId) => {
2222
return (session) => ({
2323
...session,
24-
lastLayoutUsed,
24+
lastLayout: layoutId
2525
});
2626
};
2727

2828
export const sessionActions = {
2929
reset: producerToAction(reset, sessionAtom),
30-
setLastLayoutUsed: producerToAction(setLastLayoutUsed, sessionAtom),
30+
setLastLayout: producerToAction(setLastLayout, sessionAtom),
3131
};
3232

3333
/**

packages/gephi-lite/src/core/session/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface Session {
2+
lastLayout?: string;
23
// for each layout, we save the parameters
3-
lastLayoutUsed?: string;
44
layoutsParameters: { [layout: string]: Record<string, unknown> };
55
// for each metrics, we save the parameters
66
metrics: {

packages/gephi-lite/src/views/graphPage/GraphRendering.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,18 @@ const InteractionsController: FC = () => {
8787

8888
// Get the latest layout run in the history with its data
8989
const lastLayoutActive = useMemo(() => {
90-
if (!session.lastLayoutUsed) return null;
91-
const layoutId = session.lastLayoutUsed;
90+
if (!session.lastLayout) return null;
91+
const layoutId = session.lastLayout;
92+
93+
// Special case for layout quality
94+
if(layoutId === "layout-quality")
95+
return {
96+
id: "quality",
97+
name: t(`layouts.${layoutId}.title`),
98+
type: "quality",
99+
params:{}
100+
}
101+
92102
const layout = LAYOUTS.find((e) => e.id === layoutId);
93103
if (!layout) return null;
94104

@@ -98,7 +108,7 @@ const InteractionsController: FC = () => {
98108
type: layout.type,
99109
params: session.layoutsParameters[layout.id] || {},
100110
};
101-
}, [session.lastLayoutUsed, session.layoutsParameters, t]);
111+
}, [session.lastLayout, session.layoutsParameters, t]);
102112

103113
// Get needed info to render the layout button
104114
const layoutButton = useMemo(() => {
@@ -124,6 +134,13 @@ const InteractionsController: FC = () => {
124134
disabled: false,
125135
className: "gl-btn gl-btn-icon gl-btn-outline bg-body",
126136
};
137+
} else if (lastLayoutActive.type === "quality") {
138+
result = {
139+
title: t("graph.control.layout-run-latest", { name: lastLayoutActive.name }),
140+
icon: <PlaySyncIcon />,
141+
disabled: true,
142+
className: "gl-btn gl-btn-icon gl-btn-outline bg-body",
143+
};
127144
} else {
128145
result = {
129146
title: t("graph.control.layout-start-latest", { name: lastLayoutActive.name }),

packages/gephi-lite/src/views/graphPage/index.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import cx from "classnames";
2-
import { type ComponentType, FC, useEffect, useState } from "react";
2+
import { type ComponentType, FC, useEffect, useMemo, useState } from "react";
33
import { useTranslation } from "react-i18next";
44
import { PiX } from "react-icons/pi";
55

@@ -24,7 +24,7 @@ import {
2424
MetricsIconFill,
2525
} from "../../components/common-icons";
2626
import { LayoutQualityForm } from "../../components/forms/LayoutQualityForm";
27-
import { useSelection, useSelectionActions } from "../../core/context/dataContexts";
27+
import { useLayoutState, useSelection, useSelectionActions } from "../../core/context/dataContexts";
2828
import { EVENTS, useEventsContext } from "../../core/context/eventsContext";
2929
import { LAYOUTS } from "../../core/layouts/collection";
3030
import { EDGE_METRICS, MIXED_METRICS, NODE_METRICS } from "../../core/metrics/collections";
@@ -113,10 +113,28 @@ export const GraphPage: FC = () => {
113113
const [selectedTool, setSelectedTool] = useState<undefined | { id: string; panel: ComponentType }>(undefined);
114114
const { items } = useSelection();
115115
const { emptySelection } = useSelectionActions();
116+
const layoutState = useLayoutState();
116117
const { t } = useTranslation();
117118
const isMobile = useMobile();
118119
const { emitter } = useEventsContext();
119120

121+
const menuExtended:MenuItem<{ panel?: ComponentType, isRunning?:boolean }>[] = useMemo(
122+
() =>
123+
MENU.map((section) => {
124+
if (section.id === "layout" && layoutState.type === "running" && "children" in section) {
125+
return {
126+
...section,
127+
children: section.children.map((item) => ({
128+
...item,
129+
isRunning: `layout-${layoutState.layoutId}` === item.id,
130+
})),
131+
};
132+
}
133+
return section;
134+
}),
135+
[layoutState],
136+
);
137+
120138
// Mobile display:
121139
const [expanded, setExpanded] = useState(false);
122140

@@ -190,7 +208,7 @@ export const GraphPage: FC = () => {
190208
<GraphSummary />
191209
<GraphSearchSelection />
192210
<SideMenu
193-
menu={MENU}
211+
menu={menuExtended}
194212
selected={selectedTool?.id}
195213
onSelectedChange={(item) =>
196214
setSelectedTool(

packages/gephi-lite/src/views/graphPage/panels/layouts/LayoutForm.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import Highlight from "react-highlight";
77
import { useTranslation } from "react-i18next";
88
import { Link } from "react-router";
99

10-
import { LoaderFill } from "../../../../components/Loader";
1110
import MessageAlert from "../../../../components/MessageAlert";
1211
import {
1312
CodeEditorIcon,
@@ -27,7 +26,7 @@ import { sessionAtom } from "../../../../core/session";
2726

2827
export const LayoutForm: FC<{
2928
layout: Layout;
30-
onStart: (params: Record<string, unknown>) => void;
29+
onStart: (params: Record<string, unknown>, restart?:boolean) => void;
3130
onStop: () => void;
3231
isRunning: boolean;
3332
}> = ({ layout, onStart, onStop, isRunning }) => {
@@ -74,8 +73,16 @@ export const LayoutForm: FC<{
7473
errors[param.id] = t(`error.form.max`, { ...param, name });
7574
});
7675

77-
setErrors(Object.keys(errors).length > 0 ? errors : null);
78-
}, [layout, layoutParameters, t]);
76+
const hasError = Object.keys(errors).length > 0
77+
setErrors(hasError ? errors : null);
78+
79+
if(layout.type === "worker" && !hasError && isRunning){
80+
onStart(layoutParameters, true);
81+
82+
}
83+
// I don't want to trigger this useeffect when the isRunning value changed
84+
// eslint-disable-next-line react-hooks/exhaustive-deps
85+
}, [layout, layoutParameters, t, onStart]);
7986

8087
/**
8188
* When the layout change
@@ -187,7 +194,6 @@ export const LayoutForm: FC<{
187194
param.description ? t(`layouts.${layout.id}.parameters.${param.id}.description`) : undefined
188195
}
189196
value={value as number}
190-
disabled={isRunning}
191197
onChange={(v) => changeParameter(param.id, v)}
192198
required={param.required || false}
193199
min={param.min}
@@ -203,7 +209,6 @@ export const LayoutForm: FC<{
203209
param.description ? t(`layouts.${layout.id}.parameters.${param.id}.description`) : undefined
204210
}
205211
value={!!value as boolean}
206-
disabled={isRunning}
207212
onChange={(v) => changeParameter(param.id, v)}
208213
required={param.required || false}
209214
/>
@@ -218,7 +223,6 @@ export const LayoutForm: FC<{
218223
}
219224
placeholder={t("common.none")}
220225
value={value as string}
221-
disabled={isRunning}
222226
onChange={(v) => changeParameter(param.id, v)}
223227
options={((param.itemType === "nodes" ? nodeFields : edgeFields) as FieldModel[])
224228
.filter((field) => (param.restriction ? param.restriction.includes(field.type) : true))
@@ -286,7 +290,6 @@ export const LayoutForm: FC<{
286290
</div>
287291
);
288292
})}
289-
{isRunning && <LoaderFill />}
290293
</div>
291294
</div>
292295

@@ -324,7 +327,6 @@ export const LayoutForm: FC<{
324327
const graph = getFilteredDataGraph(dataset, sigmaGraph);
325328
setParameters(getSettings(layoutParameters, graph));
326329
}}
327-
disabled={isRunning}
328330
>
329331
<GuessSettingsIcon />
330332
</button>
@@ -334,7 +336,6 @@ export const LayoutForm: FC<{
334336
title={t("common.reset")}
335337
className="gl-btn gl-btn-outline gl-btn-icon"
336338
onClick={() => setParameters()}
337-
disabled={isRunning}
338339
>
339340
<ResetIcon />
340341
</button>

0 commit comments

Comments
 (0)