Skip to content

Commit 21bb1eb

Browse files
committed
[ui] add json schema in most json viewer
1 parent 0358ca3 commit 21bb1eb

File tree

6 files changed

+307
-47
lines changed

6 files changed

+307
-47
lines changed

quickwit/quickwit-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@emotion/react": "11.14.0",
1111
"@emotion/styled": "11.14.1",
1212
"@monaco-editor/react": "4.7.0",
13+
"@openapi-contrib/openapi-schema-to-json-schema": "5.1.0",
1314
"@mui/icons-material": "7.3.5",
1415
"@mui/lab": "7.0.1-beta.19",
1516
"@mui/material": "7.3.5",

quickwit/quickwit-ui/src/components/JsonEditor.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,38 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { BeforeMount, Editor, OnMount } from "@monaco-editor/react";
16-
import { useCallback } from "react";
15+
import { BeforeMount, Editor, OnMount, useMonaco } from "@monaco-editor/react";
16+
import { useCallback, useEffect, useId } from "react";
1717
import { EDITOR_THEME } from "../utils/theme";
1818

1919
export function JsonEditor({
2020
content,
2121
resizeOnMount,
22+
jsonSchema,
2223
}: {
2324
content: unknown;
2425
resizeOnMount: boolean;
26+
jsonSchema?: object;
2527
}) {
28+
const monaco = useMonaco();
29+
const arbitraryFilename = "inmemory://" + useId();
30+
31+
// Apply json schema
32+
useEffect(() => {
33+
if (!monaco || !jsonSchema) return;
34+
35+
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
36+
validate: true,
37+
schemas: [
38+
{
39+
uri: "http://quickwit-schema.json",
40+
fileMatch: [arbitraryFilename],
41+
schema: jsonSchema,
42+
},
43+
],
44+
});
45+
}, [monaco, jsonSchema, arbitraryFilename]);
46+
2647
// Setting editor height based on lines height and count to stretch and fit its content.
2748
const onMount: OnMount = useCallback(
2849
(editor) => {
@@ -53,6 +74,7 @@ export function JsonEditor({
5374

5475
return (
5576
<Editor
77+
path={arbitraryFilename}
5678
language="json"
5779
value={JSON.stringify(content, null, 2)}
5880
beforeMount={beforeMount}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2021-Present Datadog, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { openapiSchemaToJsonSchema } from "@openapi-contrib/openapi-schema-to-json-schema";
16+
import React from "react";
17+
18+
/**
19+
* return the json schema for the given component
20+
* based on the openapi schema at /openapi.json
21+
*
22+
* @param ref is a path to the component, usually starting with #/components/schemas/...
23+
*/
24+
export const useJsonSchema = (ref: string) => {
25+
const [openApiSchema, setOpenApiSchema] = React.useState<any>(null);
26+
27+
console.log(openApiSchema);
28+
29+
React.useEffect(() => {
30+
schemaPromise = schemaPromise || fetchOpenApiSchema();
31+
schemaPromise.then(setOpenApiSchema);
32+
}, []);
33+
34+
const jsonShema = React.useMemo(() => {
35+
if (!openApiSchema) return null;
36+
return openapiSchemaToJsonSchema({
37+
...openApiSchema,
38+
$ref: ref,
39+
});
40+
}, [openApiSchema, ref]);
41+
42+
return jsonShema;
43+
};
44+
45+
let schemaPromise: Promise<any> | null = null;
46+
export const fetchOpenApiSchema = async () => {
47+
const response = await fetch("/openapi.json");
48+
if (!response.ok) {
49+
throw new Error(`Failed to fetch OpenAPI schema: ${response.statusText}`);
50+
}
51+
return await response.json();
52+
};

quickwit/quickwit-ui/src/views/ClusterView.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import Loader from "../components/Loader";
2525
import ErrorResponseDisplay from "../components/ResponseErrorDisplay";
2626
import { Client } from "../services/client";
27+
import { useJsonSchema } from "../services/jsonShema";
2728
import { Cluster, ResponseError } from "../utils/models";
2829

2930
function ClusterView() {
@@ -56,9 +57,18 @@ function ClusterView() {
5657
if (loading || cluster == null) {
5758
return <Loader />;
5859
}
59-
return <JsonEditor content={cluster} resizeOnMount={false} />;
60+
return (
61+
<JsonEditor
62+
jsonSchema={jsonSchema}
63+
content={cluster}
64+
resizeOnMount={false}
65+
/>
66+
);
6067
};
6168

69+
const jsonSchema =
70+
useJsonSchema("#/components/schemas/ClusterSnapshot") ?? undefined;
71+
6272
return (
6373
<ViewUnderAppBarBox>
6474
<FullBoxContainer>

quickwit/quickwit-ui/src/views/IndexView.tsx

Lines changed: 126 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from "../components/LayoutUtils";
2828
import Loader from "../components/Loader";
2929
import { Client } from "../services/client";
30+
import { useJsonSchema } from "../services/jsonShema";
3031
import { Index } from "../utils/models";
3132

3233
export type ErrorResult = {
@@ -53,14 +54,18 @@ function IndexView() {
5354
const { indexId } = useParams();
5455
const [loading, setLoading] = useState(false);
5556
const [, setLoadingError] = useState<ErrorResult | null>(null);
56-
const [tabIndex, setTabIndex] = useState("1");
57+
const [tab, setTab] = useState<
58+
| "summary"
59+
| "sources"
60+
| "doc-mapping"
61+
| "indexing-settings"
62+
| "search-settings"
63+
| "retention-settings"
64+
| "splits"
65+
>("summary");
5766
const [index, setIndex] = useState<Index>();
5867
const quickwitClient = useMemo(() => new Client(), []);
5968

60-
const handleTabIndexChange = (_: React.SyntheticEvent, newValue: string) => {
61-
setTabIndex(newValue);
62-
};
63-
6469
const fetchIndex = useCallback(() => {
6570
setLoading(true);
6671
if (indexId === undefined) {
@@ -94,53 +99,41 @@ function IndexView() {
9499
height: "calc(100% - 48px)",
95100
}}
96101
>
97-
<TabContext value={tabIndex}>
102+
<TabContext value={tab}>
98103
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
99-
<TabList onChange={handleTabIndexChange} aria-label="Index tabs">
100-
<Tab label="Summary" value="1" />
101-
<Tab label="Sources" value="2" />
102-
<Tab label="Doc Mapping" value="3" />
103-
<Tab label="Indexing settings" value="4" />
104-
<Tab label="Search settings" value="5" />
105-
<Tab label="Retention settings" value="6" />
106-
<Tab label="Splits" value="7" />
104+
<TabList
105+
onChange={(_, newTab) => setTab(newTab)}
106+
aria-label="Index tabs"
107+
>
108+
<Tab label="Summary" value="summary" />
109+
<Tab label="Sources" value="sources" />
110+
<Tab label="Doc Mapping" value="doc-mapping" />
111+
<Tab label="Indexing settings" value="indexing-settings" />
112+
<Tab label="Search settings" value="search-settings" />
113+
<Tab label="Retention settings" value="retention-settings" />
114+
<Tab label="Splits" value="splits" />
107115
</TabList>
108116
</Box>
109-
<CustomTabPanel value="1">
110-
<IndexSummary index={index} />
117+
<CustomTabPanel value="summary">
118+
<SummaryTab index={index} />
111119
</CustomTabPanel>
112-
<CustomTabPanel value="2">
113-
<JsonEditor
114-
content={index.metadata.sources}
115-
resizeOnMount={false}
116-
/>
120+
<CustomTabPanel value="sources">
121+
<SourcesTab index={index} />
117122
</CustomTabPanel>
118-
<CustomTabPanel value="3">
119-
<JsonEditor
120-
content={index.metadata.index_config.doc_mapping}
121-
resizeOnMount={false}
122-
/>
123+
<CustomTabPanel value="doc-mapping">
124+
<DocMappingTab index={index} />
123125
</CustomTabPanel>
124-
<CustomTabPanel value="4">
125-
<JsonEditor
126-
content={index.metadata.index_config.indexing_settings}
127-
resizeOnMount={false}
128-
/>
126+
<CustomTabPanel value="indexing-settings">
127+
<IndexingSettingsTab index={index} />
129128
</CustomTabPanel>
130-
<CustomTabPanel value="5">
131-
<JsonEditor
132-
content={index.metadata.index_config.search_settings}
133-
resizeOnMount={false}
134-
/>
129+
<CustomTabPanel value="search-settings">
130+
<SearchSettingsTab index={index} />
135131
</CustomTabPanel>
136-
<CustomTabPanel value="6">
137-
<JsonEditor
138-
content={index.metadata.index_config.retention || {}}
139-
resizeOnMount={false}
140-
/>
132+
<CustomTabPanel value="retention-settings">
133+
<RetentionSettingsTab index={index} />
141134
</CustomTabPanel>
142-
<CustomTabPanel value="7">
143-
<JsonEditor content={index.splits} resizeOnMount={false} />
135+
<CustomTabPanel value="splits">
136+
<SplitsTab index={index} />
144137
</CustomTabPanel>
145138
</TabContext>
146139
</Box>
@@ -169,3 +162,93 @@ function IndexView() {
169162
}
170163

171164
export default IndexView;
165+
166+
function SummaryTab({ index }: { index: Index }) {
167+
return <IndexSummary index={index} />;
168+
}
169+
170+
function SourcesTab({ index }: { index: Index }) {
171+
const jsonSchema =
172+
useJsonSchema(
173+
"#/components/schemas/IndexMetadataV0_8/properties/sources",
174+
) ?? undefined;
175+
176+
return (
177+
<JsonEditor
178+
content={index.metadata.sources}
179+
resizeOnMount={false}
180+
jsonSchema={jsonSchema}
181+
/>
182+
);
183+
}
184+
185+
function DocMappingTab({ index }: { index: Index }) {
186+
const jsonSchema =
187+
useJsonSchema("#/components/schemas/DocMapping") ?? undefined;
188+
return (
189+
<JsonEditor
190+
content={index.metadata.index_config.doc_mapping}
191+
resizeOnMount={false}
192+
jsonSchema={jsonSchema}
193+
/>
194+
);
195+
}
196+
197+
function IndexingSettingsTab({ index }: { index: Index }) {
198+
const jsonSchema =
199+
useJsonSchema("#/components/schemas/IndexingSettings") ?? undefined;
200+
201+
return (
202+
<JsonEditor
203+
content={index.metadata.index_config.indexing_settings}
204+
resizeOnMount={false}
205+
jsonSchema={jsonSchema}
206+
/>
207+
);
208+
}
209+
210+
function SearchSettingsTab({ index }: { index: Index }) {
211+
const jsonSchema =
212+
useJsonSchema("#/components/schemas/SearchSettings") ?? undefined;
213+
214+
return (
215+
<JsonEditor
216+
content={index.metadata.index_config.search_settings}
217+
resizeOnMount={false}
218+
jsonSchema={jsonSchema}
219+
/>
220+
);
221+
}
222+
223+
function RetentionSettingsTab({ index }: { index: Index }) {
224+
const jsonSchema =
225+
useJsonSchema("#/components/schemas/RetentionPolicy") ?? undefined;
226+
227+
return (
228+
<JsonEditor
229+
content={index.metadata.index_config.retention || {}}
230+
resizeOnMount={false}
231+
jsonSchema={jsonSchema}
232+
/>
233+
);
234+
}
235+
236+
function SplitsTab({ index }: { index: Index }) {
237+
const splitShema = useJsonSchema("#/components/schemas/Split");
238+
const jsonSchema =
239+
(splitShema && {
240+
...splitShema,
241+
$ref: undefined,
242+
type: "array",
243+
items: { $ref: "#/components/schemas/Split" },
244+
}) ??
245+
undefined;
246+
247+
return (
248+
<JsonEditor
249+
content={index.splits}
250+
resizeOnMount={false}
251+
jsonSchema={jsonSchema}
252+
/>
253+
);
254+
}

0 commit comments

Comments
 (0)