Skip to content

Commit 609a7ea

Browse files
committed
feat: Proof of concept of mobx
1 parent 938ca57 commit 609a7ea

File tree

4 files changed

+385
-144
lines changed

4 files changed

+385
-144
lines changed

app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
"material-ui-popup-state": "^2.0.0",
8787
"micro-cors": "^0.1.1",
8888
"mitt": "^3.0.0",
89+
"mobx": "^6.7.0",
90+
"mobx-react": "^7.6.0",
8991
"nanoid": "^3.1.12",
9092
"next-auth": "^4.10.3",
9193
"nprogress": "^0.2.0",

app/pages/_mobx.tsx

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { ExpandMore, ChevronRight } from "@mui/icons-material";
2+
import { TreeItem, TreeView } from "@mui/lab";
3+
import {
4+
CircularProgress,
5+
Button,
6+
Typography,
7+
Box,
8+
TextField,
9+
} from "@mui/material";
10+
import keyBy from "lodash/keyBy";
11+
import {
12+
observable,
13+
makeObservable,
14+
computed,
15+
flow,
16+
configure,
17+
ObservableMap,
18+
} from "mobx";
19+
import { observer } from "mobx-react-lite";
20+
import { useMemo, useState } from "react";
21+
import { Client, OperationResult, useClient } from "urql";
22+
23+
import { DataSource } from "@/configurator";
24+
import {
25+
DataCubeMetadataWithComponentValuesDocument,
26+
DataCubeMetadataWithComponentValuesQuery,
27+
DataCubeMetadataWithComponentValuesQueryVariables,
28+
Dimension,
29+
DimensionHierarchyDocument,
30+
DimensionHierarchyQuery,
31+
DimensionHierarchyQueryVariables,
32+
HierarchyValue,
33+
Measure,
34+
} from "@/graphql/query-hooks";
35+
import { visitHierarchy } from "@/rdf/tree-utils";
36+
37+
configure({ enforceActions: "always" });
38+
39+
type QueryResult<T> = {
40+
fetchStatus: "loaded" | "loading" | "idle";
41+
error: Error | undefined;
42+
data: T | undefined;
43+
};
44+
45+
class Cube {
46+
client: Client;
47+
iri: string;
48+
source: DataSource;
49+
dimensions: undefined | Dimension[];
50+
measures: undefined | Measure[];
51+
loading: boolean;
52+
hierarchies: ObservableMap<string, QueryResult<HierarchyValue[]>>;
53+
54+
constructor(client: Client, iri: string, source: DataSource) {
55+
this.client = client;
56+
this.iri = iri;
57+
this.source = source;
58+
this.loading = false;
59+
this.dimensions = undefined;
60+
this.measures = undefined;
61+
this.hierarchies = observable.map<string, QueryResult<HierarchyValue[]>>();
62+
makeObservable(this, {
63+
loading: observable,
64+
dimensions: observable.struct,
65+
measures: observable.struct,
66+
components: computed,
67+
componentsByIri: computed,
68+
hierarchies: observable,
69+
hierarchyParents: computed,
70+
load: flow,
71+
loadHierarchy: flow,
72+
});
73+
}
74+
75+
*load() {
76+
const vars: DataCubeMetadataWithComponentValuesQueryVariables = {
77+
iri: this.iri,
78+
locale: "en",
79+
sourceType: this.source.type,
80+
sourceUrl: this.source.url,
81+
};
82+
83+
try {
84+
this.loading = true;
85+
const res: OperationResult<DataCubeMetadataWithComponentValuesQuery> =
86+
yield this.client
87+
.query<DataCubeMetadataWithComponentValuesQuery>(
88+
DataCubeMetadataWithComponentValuesDocument,
89+
vars
90+
)
91+
.toPromise();
92+
93+
this.dimensions = res.data?.dataCubeByIri?.dimensions;
94+
this.measures = res.data?.dataCubeByIri?.measures;
95+
this.loading = false;
96+
for (let component of this.components) {
97+
this.hierarchies.set(
98+
component.iri,
99+
observable.object({
100+
fetchStatus: "idle",
101+
error: undefined,
102+
data: undefined,
103+
})
104+
);
105+
}
106+
} finally {
107+
this.loading = false;
108+
}
109+
}
110+
111+
*loadHierarchy(dimensionIri: string) {
112+
const vars: DimensionHierarchyQueryVariables = {
113+
cubeIri: this.iri,
114+
dimensionIri,
115+
locale: "en",
116+
sourceType: this.source.type,
117+
sourceUrl: this.source.url,
118+
};
119+
120+
const ourHierarchy = this.hierarchies.get(dimensionIri);
121+
if (!ourHierarchy) {
122+
return;
123+
}
124+
ourHierarchy.fetchStatus = "loading";
125+
try {
126+
const res: OperationResult<DimensionHierarchyQuery> = yield this.client
127+
.query<DimensionHierarchyQuery>(DimensionHierarchyDocument, vars)
128+
.toPromise();
129+
const hierarchy = res.data?.dataCubeByIri?.dimensionByIri?.hierarchy;
130+
if (hierarchy) {
131+
ourHierarchy.data = hierarchy;
132+
}
133+
} finally {
134+
ourHierarchy.fetchStatus = "loaded";
135+
}
136+
}
137+
138+
get components() {
139+
return [
140+
...(this.dimensions?.slice() || []),
141+
...(this.measures?.slice() || []),
142+
];
143+
}
144+
145+
get hierarchyParents() {
146+
const res: Record<
147+
Dimension["iri"],
148+
Record<HierarchyValue["value"], HierarchyValue["value"]>
149+
> = {};
150+
for (let k of this.hierarchies.keys()) {
151+
const parents: Record<HierarchyValue["value"], HierarchyValue["value"]> =
152+
{};
153+
res[k] = parents;
154+
const hierarchy = this.hierarchies.get(k)?.data!;
155+
visitHierarchy(hierarchy, (node) => {
156+
for (let c of node.children || []) {
157+
parents[c.value] = node.value;
158+
}
159+
});
160+
}
161+
return res;
162+
}
163+
164+
get componentsByIri() {
165+
return keyBy(this.components, (x) => x.iri);
166+
}
167+
}
168+
169+
const HierarchyTree = observer(
170+
({ cube, dimensionIri }: { cube: Cube; dimensionIri: string }) => {
171+
const render = (node: HierarchyValue) => {
172+
return (
173+
<TreeItem key={node.value} label={node.label} nodeId={node.value}>
174+
{node?.children?.map((c) => render(c))}
175+
</TreeItem>
176+
);
177+
};
178+
const hierarchy = cube.hierarchies.get(dimensionIri);
179+
if (!hierarchy?.data) {
180+
return <Typography color="info">No hierarchy</Typography>;
181+
}
182+
183+
return hierarchy?.data ? (
184+
<TreeView
185+
defaultCollapseIcon={<ExpandMore />}
186+
defaultExpandIcon={<ChevronRight />}
187+
sx={{ flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
188+
>
189+
{hierarchy.data.map((h) => render(h))}
190+
</TreeView>
191+
) : null;
192+
}
193+
);
194+
195+
const CubeView = observer(({ cube }: { cube: Cube }) => {
196+
return (
197+
<span>
198+
{cube.loading ? <CircularProgress size={10} /> : null}
199+
<Box
200+
component="ul"
201+
sx={{
202+
listStyleType: "none",
203+
pl: 0,
204+
ml: 0,
205+
"& > * + *": { marginTop: "1rem" },
206+
}}
207+
>
208+
{cube.components.map((c) => {
209+
const hierarchy = cube.hierarchies.get(c.iri);
210+
const fetchStatus = hierarchy?.fetchStatus;
211+
return (
212+
<li key={c.iri}>
213+
<Typography variant="h4">{c.label}</Typography>
214+
<Typography variant="caption">{c.description}</Typography>
215+
216+
<div>
217+
<Typography
218+
variant="h5"
219+
sx={{ minHeight: 40, display: "flex", alignItems: "center" }}
220+
>
221+
Hierarchy
222+
{fetchStatus === "loaded" ? null : (
223+
<Button
224+
variant="text"
225+
size="small"
226+
onClick={() => cube.loadHierarchy(c.iri)}
227+
>
228+
{fetchStatus === "idle" ? "load" : null}
229+
{fetchStatus === "loading" ? (
230+
<CircularProgress color="secondary" />
231+
) : null}
232+
</Button>
233+
)}
234+
</Typography>
235+
</div>
236+
{cube.hierarchies.get(c.iri)?.fetchStatus === "loaded" ? (
237+
<HierarchyTree cube={cube} dimensionIri={c.iri} />
238+
) : (
239+
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
240+
not loaded
241+
</Typography>
242+
)}
243+
</li>
244+
);
245+
})}
246+
</Box>
247+
<button onClick={() => cube.load()}>Load</button>
248+
</span>
249+
);
250+
});
251+
252+
const Page = observer(() => {
253+
const [iri, setIri] = useState<string>(
254+
"https://environment.ld.admin.ch/foen/fab_Offentliche_Ausgaben_test3/8"
255+
);
256+
const client = useClient();
257+
const cube = useMemo(() => {
258+
if (!iri) {
259+
return;
260+
}
261+
const cube = new Cube(client, iri, {
262+
type: "sparql",
263+
url: "https://int.lindas.admin.ch/query",
264+
});
265+
cube.load();
266+
return cube;
267+
}, [client, iri]);
268+
return (
269+
<Box sx={{ margin: "auto", width: 800, my: "2rem" }}>
270+
<TextField
271+
label="Cube IRI"
272+
type="text"
273+
value={iri}
274+
onChange={(ev) => setIri(ev.target.value)}
275+
/>
276+
{cube ? <CubeView key={iri} cube={cube} /> : <div>Choose an iri</div>}
277+
</Box>
278+
);
279+
});
280+
281+
export default Page;

app/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
},
2121
"noImplicitAny": true,
2222
"noUnusedLocals": true,
23-
"noUnusedParameters": true
23+
"noUnusedParameters": true,
24+
"emitDecoratorMetadata": true,
25+
"experimentalDecorators": true
2426
},
2527
"exclude": ["node_modules"],
2628
"include": [

0 commit comments

Comments
 (0)