Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions quickwit/quickwit-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@monaco-editor/react": "4.7.0",
"@openapi-contrib/openapi-schema-to-json-schema": "5.1.0",
"@mui/icons-material": "7.3.5",
"@mui/lab": "7.0.1-beta.19",
"@mui/material": "7.3.5",
Expand Down
37 changes: 34 additions & 3 deletions quickwit/quickwit-ui/src/components/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,42 @@
// See the License for the specific language governing permissions and
// limitations under the License.

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

export function JsonEditor({
content,
readOnly = true,
resizeOnMount,
jsonSchema,
onContentEdited,
}: {
content: unknown;
readOnly?: boolean;
resizeOnMount: boolean;
jsonSchema?: object;
onContentEdited?: (value: unknown) => void;
}) {
const monaco = useMonaco();
const arbitraryFilename = "inmemory://" + useId();

// Apply json schema
useEffect(() => {
if (!monaco || !jsonSchema) return;

monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
{
uri: "http://quickwit-schema.json",
fileMatch: [arbitraryFilename],
schema: jsonSchema,
},
],
});
}, [monaco, jsonSchema, arbitraryFilename]);

// Setting editor height based on lines height and count to stretch and fit its content.
const onMount: OnMount = useCallback(
(editor) => {
Expand Down Expand Up @@ -53,12 +78,18 @@ export function JsonEditor({

return (
<Editor
path={arbitraryFilename}
language="json"
value={JSON.stringify(content, null, 2)}
onChange={(value) => {
try {
if (value) onContentEdited?.(JSON.parse(value));
} catch (err) {}
}}
beforeMount={beforeMount}
onMount={onMount}
options={{
readOnly: true,
readOnly: readOnly,
fontFamily: "monospace",
overviewRulerBorder: false,
overviewRulerLanes: 0,
Expand Down
98 changes: 98 additions & 0 deletions quickwit/quickwit-ui/src/components/JsonEditorEditable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2021-Present Datadog, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import CheckIcon from "@mui/icons-material/Check";
import EditIcon from "@mui/icons-material/Edit";
import { Box, Button, Chip, Stack } from "@mui/material";
import { useEffect, useState } from "react";
import { JsonEditor } from "./JsonEditor";

type JsonEditorEditableProps = {
saving: boolean;
pristine: boolean;
onSave: () => void;
} & React.ComponentProps<typeof JsonEditor>;

/**
* wrapper around JsonEditor that displays edit actions
*/
export function JsonEditorEditable({
saving,
pristine,
onSave,
...jsonEditorProps
}: JsonEditorEditableProps) {
const wasSaving = useDelayedTruthyValue(saving, 1000);
const showSuccess = !saving && wasSaving;

return (
<Box style={{ height: "100%", position: "relative" }}>
<JsonEditor readOnly={false} {...jsonEditorProps} />
<Stack
direction="row"
spacing={1}
style={{ position: "absolute", top: 8, right: 12, zIndex: 1001 }}
>
{pristine && !showSuccess && (
<Chip
icon={<EditIcon sx={{ fontSize: 16 }} />}
label="Editable"
size="small"
variant="filled"
/>
)}
{(!pristine || showSuccess) && (
<Button
variant="contained"
size="small"
onClick={onSave}
disabled={saving || showSuccess}
style={{ width: "100px" }}
>
{saving && "Saving..."}
{showSuccess && (
<>
<CheckIcon sx={{ fontSize: 16, mr: 0.5 }} />
Saved
</>
)}
{!saving && !showSuccess && "Save"}
</Button>
)}
</Stack>
</Box>
);
}

/**
* Returns the value immediately when truthy, but delays returning falsy values by delayMs.
* Useful for showing success states briefly after an operation completes.
*/
function useDelayedTruthyValue<T>(value: T, delayMs: number): T {
const [delayedValue, setDelayedValue] = useState<T>(value);

useEffect(() => {
if (value) {
setDelayedValue(value);
} else {
const timeout = setTimeout(() => {
setDelayedValue(value);
}, delayMs);

return () => clearTimeout(timeout);
}
}, [value, delayMs]);

return value || delayedValue;
}
14 changes: 13 additions & 1 deletion quickwit/quickwit-ui/src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,25 @@ export class Client {
return this.fetch(`${this.apiRoot()}indexes`, {});
}

// TODO unit test
async updateIndexConfig(
indexId: string,
indexConfig: IndexMetadata["index_config"],
): Promise<IndexMetadata> {
return this.fetch(
`${this.apiRoot()}indexes/${indexId}`,
{ method: "PUT" },
JSON.stringify(indexConfig),
);
}

async fetch<T>(
url: string,
params: RequestInit,
body: string | null = null,
): Promise<T> {
if (body !== null) {
params.method = "POST";
params.method = params.method ?? "POST";
params.body = body;
params.headers = {
...params.headers,
Expand Down
52 changes: 52 additions & 0 deletions quickwit/quickwit-ui/src/services/jsonShema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2021-Present Datadog, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { openapiSchemaToJsonSchema } from "@openapi-contrib/openapi-schema-to-json-schema";
import React from "react";

/**
* return the json schema for the given component
* based on the openapi schema at /openapi.json
*
* @param ref is a path to the component, usually starting with #/components/schemas/...
*/
export const useJsonSchema = (ref: string) => {
const [openApiSchema, setOpenApiSchema] = React.useState<any>(null);

console.log(openApiSchema);

React.useEffect(() => {
schemaPromise = schemaPromise || fetchOpenApiSchema();
schemaPromise.then(setOpenApiSchema);
}, []);

const jsonShema = React.useMemo(() => {
if (!openApiSchema) return null;
return openapiSchemaToJsonSchema({
...openApiSchema,
$ref: ref,
});
}, [openApiSchema, ref]);

return jsonShema;
};

let schemaPromise: Promise<any> | null = null;
export const fetchOpenApiSchema = async () => {
const response = await fetch("/openapi.json");
if (!response.ok) {
throw new Error(`Failed to fetch OpenAPI schema: ${response.statusText}`);
}
return await response.json();
};
12 changes: 11 additions & 1 deletion quickwit/quickwit-ui/src/views/ClusterView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import Loader from "../components/Loader";
import ErrorResponseDisplay from "../components/ResponseErrorDisplay";
import { Client } from "../services/client";
import { useJsonSchema } from "../services/jsonShema";
import { Cluster, ResponseError } from "../utils/models";

function ClusterView() {
Expand Down Expand Up @@ -56,9 +57,18 @@ function ClusterView() {
if (loading || cluster == null) {
return <Loader />;
}
return <JsonEditor content={cluster} resizeOnMount={false} />;
return (
<JsonEditor
jsonSchema={jsonSchema}
content={cluster}
resizeOnMount={false}
/>
);
};

const jsonSchema =
useJsonSchema("#/components/schemas/ClusterSnapshot") ?? undefined;

return (
<ViewUnderAppBarBox>
<FullBoxContainer>
Expand Down
Loading
Loading