Przeglądaj źródła

feat: playground (#809)

* init play page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* adjust style

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* style

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update selection color

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* ajust indent space length

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init autocompletion box

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* store code in localstorage

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init milvus http lezer grammer

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update grammer

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update grammer

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* adjust grammer

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init highlight

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init highlight request

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* adjust highlight block

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* refactor structure

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* refactor

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* refactor

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* rename

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* feat: playground api

* init linter

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* feat: playground toolbar decoration

* lint url

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update languages and selectionDecoration

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix extension crash if no node found

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* feat: response preview

* fix selection extension crash

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init tab completion

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* feat: playground url autocomplete

* feat: autocomplete keymap

* feat: playground body autocomplete

* style: autocomplete

* feat: use-codemirror hook

* feat: playground macro

* fix: playground token

* style: playground

* feat: codelens decoration

* fix: codelens error

* pref: codelens

* feat: codelens shortcuts

* fix: playground request

* fix: dependencies

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Co-authored-by: ryjiang <jiangruiyi@gmail.com>
Shuyoou 1 miesiąc temu
rodzic
commit
156f3a4403
41 zmienionych plików z 4465 dodań i 8 usunięć
  1. 1 0
      .gitignore
  2. 6 0
      client/package.json
  3. 5 1
      client/src/components/code/CodeBlock.tsx
  4. 1 0
      client/src/components/code/Types.ts
  5. 1 0
      client/src/consts/Localstorage.ts
  6. 1 0
      client/src/consts/index.ts
  7. 4 0
      client/src/consts/link.ts
  8. 10 0
      client/src/hooks/Navigation.tsx
  9. 1 0
      client/src/i18n/cn/nav.ts
  10. 1 0
      client/src/i18n/en/nav.ts
  11. 101 0
      client/src/openapi/index.js
  12. 9 5
      client/src/pages/index.tsx
  13. 146 0
      client/src/pages/play/Play.tsx
  14. 34 0
      client/src/pages/play/Types.ts
  15. 171 0
      client/src/pages/play/hooks/use-codemirror.ts
  16. 323 0
      client/src/pages/play/language/extensions/autocomplete.ts
  17. 124 0
      client/src/pages/play/language/extensions/autocompletion.ts
  18. 185 0
      client/src/pages/play/language/extensions/codelens.ts
  19. 23 0
      client/src/pages/play/language/extensions/consts.ts
  20. 690 0
      client/src/pages/play/language/extensions/gutter.ts
  21. 19 0
      client/src/pages/play/language/extensions/highlights.ts
  22. 1781 0
      client/src/pages/play/language/extensions/json/openapi.json
  23. 7 0
      client/src/pages/play/language/extensions/keymap.ts
  24. 42 0
      client/src/pages/play/language/extensions/linter.ts
  25. 115 0
      client/src/pages/play/language/extensions/selectionDecoration.ts
  26. 112 0
      client/src/pages/play/language/milvus.http.grammar
  27. 11 0
      client/src/pages/play/language/milvus.http.parser.js
  28. 29 0
      client/src/pages/play/language/milvus.http.parser.terms.js
  29. 46 0
      client/src/pages/play/language/milvus.http.ts
  30. 222 0
      client/src/pages/play/style.tsx
  31. 29 0
      client/src/pages/play/utils/event.ts
  32. 2 0
      client/src/pages/play/utils/index.ts
  33. 46 0
      client/src/pages/play/utils/request.ts
  34. 2 0
      client/src/router/Router.tsx
  35. 1 0
      client/src/router/consts.ts
  36. 58 2
      client/yarn.lock
  37. 2 0
      server/src/app.ts
  38. 27 0
      server/src/playground/dto.ts
  39. 8 0
      server/src/playground/index.ts
  40. 41 0
      server/src/playground/playground.controller.ts
  41. 28 0
      server/src/playground/playground.service.ts

+ 1 - 0
.gitignore

@@ -20,6 +20,7 @@ node_modules
 /client/.env.test.local
 /client/.env.production.local
 /client/report.*.json
+/client/src/openapi/openapi.json
 
 /client/npm-debug.log*
 /client/yarn-debug.log*

+ 6 - 0
client/package.json

@@ -6,6 +6,7 @@
   "bugs": "https://github.com/zilliztech/attu/issues",
   "private": false,
   "dependencies": {
+    "@codemirror/autocomplete": "^6.18.4",
     "@codemirror/commands": "^6.6.0",
     "@codemirror/lang-javascript": "^6.2.2",
     "@codemirror/lang-json": "^6.0.1",
@@ -17,6 +18,8 @@
     "@ddietr/codemirror-themes": "^1.4.2",
     "@emotion/react": "^11.13.0",
     "@emotion/styled": "^11.13.0",
+    "@lezer/common": "^1.2.3",
+    "@lezer/highlight": "^1.2.1",
     "@json2csv/plainjs": "^7.0.6",
     "@mui/icons-material": "^5.17.1",
     "@mui/material": "^5.17.1",
@@ -41,6 +44,8 @@
     "web-vitals": "^1.0.1"
   },
   "devDependencies": {
+    "@lezer/generator": "^1.7.2",
+    "@types/codemirror": "^5.60.15",
     "@types/d3": "^7.4.3",
     "@types/file-saver": "^2.0.5",
     "@types/node": "^12.0.0",
@@ -60,6 +65,7 @@
   "scripts": {
     "start": "vite",
     "build": "vite build",
+    "lezer": "lezer-generator src/pages/play/language/milvus.http.grammar -o  src/pages/play/language/milvus.http.parser.js",
     "format": "prettier --write '**/*.{ts,js,tsx,jsx,css}'"
   },
   "eslintConfig": {

+ 5 - 1
client/src/components/code/CodeBlock.tsx

@@ -19,6 +19,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     right: theme.spacing(1),
     '& svg': {
       width: 16,
+      height: 16,
     },
   },
 }));
@@ -31,12 +32,15 @@ const CodeBlock: FC<CodeBlockProps> = ({
   code,
   language,
   wrapperClass = '',
+  style = {},
 }) => {
   const theme = useTheme();
   const classes = getStyles();
 
   const { t: commonTrans } = useTranslation();
 
+  const highlightTheme = theme.palette.mode === 'dark' ? vs2015 : github;
+
   return (
     <div className={`${classes.wrapper} ${wrapperClass}`}>
       <CopyButton
@@ -46,7 +50,7 @@ const CodeBlock: FC<CodeBlockProps> = ({
       />
       <SyntaxHighlighter
         language={language}
-        style={theme.palette.mode === 'dark' ? vs2015 : github}
+        style={{ ...highlightTheme, ...style }}
         customStyle={CodeStyle}
         showLineNumbers={true}
       >

+ 1 - 0
client/src/components/code/Types.ts

@@ -15,6 +15,7 @@ export interface CodeBlockProps {
   code: string;
   language: string;
   wrapperClass?: string;
+  style?: Record<string, React.CSSProperties>;
 }
 
 export interface CodeViewData extends CodeBlockProps {

+ 1 - 0
client/src/consts/Localstorage.ts

@@ -15,3 +15,4 @@ export const LAST_TIME_HEALTHY_THRESHOLD_MEMORY =
 export const ATTU_UI_TREE_WIDTH = 'attu.ui.tree.with';
 export const ATTU_AUTH_HISTORY = 'attu.auth.history';
 export const ATTU_THEME_MODE = 'attu.theme.mode';
+export const ATTU_PLAY_CODE = 'attu.play.code';

+ 1 - 0
client/src/consts/index.ts

@@ -5,3 +5,4 @@ export * from './Prometheus';
 export * from './Util';
 export * from './ui';
 export * from './default';
+export * from './link';

+ 4 - 0
client/src/consts/link.ts

@@ -0,0 +1,4 @@
+export const MILVUS_BASE_URL = 'https://milvus.io';
+export const CLOUD_DOCS_BASE_URL = `https://docs.zilliz.com`;
+export const MILVUS_RESTFUL_DOC_URL = `${MILVUS_BASE_URL}/api-reference/restful/v2.5.x/About.md`;
+export const CLOUD_RESTFUL_DOC_URL = `${CLOUD_DOCS_BASE_URL}/reference/restful/data-plane-v2`;

+ 10 - 0
client/src/hooks/Navigation.tsx

@@ -76,6 +76,16 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         break;
       }
+      case ALL_ROUTER_TYPES.PLAY: {
+        const navInfo: NavInfo = {
+          navTitle: navTrans('play'),
+          backPath: '',
+          showDatabaseSelector: false,
+        };
+        setNavInfo(navInfo);
+        break;
+      }
+
       default:
         break;
     }

+ 1 - 0
client/src/i18n/cn/nav.ts

@@ -7,6 +7,7 @@ const navTrans = {
   system: '系统视图',
   user: '用户和角色',
   database: '数据库',
+  play: 'Play',
 };
 
 export default navTrans;

+ 1 - 0
client/src/i18n/en/nav.ts

@@ -7,6 +7,7 @@ const navTrans = {
   system: 'System View',
   user: 'User and Role',
   database: 'Database',
+  play: 'Play',
 };
 
 export default navTrans;

+ 101 - 0
client/src/openapi/index.js

@@ -0,0 +1,101 @@
+/**
+ * https://github.com/zilliztech/zdoc/blob/master/plugins/apifox-docs/meta/openapi.json
+ *
+ * Copy github openapi.json to client/src/openapi/openapi.json
+ * Run: yarn openapi
+ *
+ * Or set GITHUB_TOKEN to fetch openapi.json from github
+ * Run: GITHUB_TOKEN=ghpxxx yarn openapi
+ */
+
+const fs = require('fs');
+
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
+const sourcePath = 'src/openapi/openapi.json';
+const targetPath = 'src/pages/play/language/extensions/json/openapi.json';
+
+async function fetchOpenApiJson() {
+  const url =
+    'https://api.github.com/repos/zilliztech/zdoc/contents/plugins/apifox-docs/meta/openapi.json';
+
+  try {
+    const response = await fetch(url, {
+      headers: {
+        Accept: 'application/vnd.github.v3.raw',
+        Authorization: `Bearer ${GITHUB_TOKEN}`,
+      },
+    });
+
+    if (!response.ok) {
+      throw new Error(`Error fetching file: ${response.statusText}`);
+    }
+
+    return await response.json();
+  } catch (error) {
+    console.error('Error:', error);
+  }
+}
+
+function getProperties(schema) {
+  const result = [];
+  const { properties, required = [] } = schema;
+
+  if (!properties) {
+    return [];
+  }
+
+  for (const [key, value] of Object.entries(properties)) {
+    const { type } = value;
+    result.push({
+      name: key,
+      type,
+      required: required.includes(key),
+      children: getProperties(value),
+    });
+  }
+  return result;
+}
+
+async function bootstrap() {
+  const openapiData = GITHUB_TOKEN
+    ? await fetchOpenApiJson()
+    : JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
+  const { paths } = openapiData;
+  const IdentifierMapArr = [];
+
+  for (const [path, detail] of Object.entries(paths)) {
+    if (path.startsWith('/v2/vectordb')) {
+      const pathParts = path.split('/').filter(part => part); // Split and filter empty parts
+      const [, , resource, ...rest] = pathParts;
+      const action = rest.join('/');
+      const resourceIndex = IdentifierMapArr.findIndex(
+        item => item.name === resource
+      );
+
+      const { requestBody } = detail.post ?? {};
+      const properties = requestBody
+        ? getProperties(requestBody.content['application/json'].schema)
+        : [];
+
+      if (resourceIndex < 0) {
+        IdentifierMapArr.push({
+          name: resource,
+          children: [{ name: action, children: properties }],
+        });
+      } else {
+        const actionIndex = IdentifierMapArr[resourceIndex].children.findIndex(
+          item => item.name === action
+        );
+        if (actionIndex < 0) {
+          IdentifierMapArr[resourceIndex].children.push({
+            name: action,
+            children: properties,
+          });
+        }
+      }
+    }
+  }
+
+  fs.writeFileSync(targetPath, JSON.stringify(IdentifierMapArr, null, 2));
+}
+bootstrap();

+ 9 - 5
client/src/pages/index.tsx

@@ -76,6 +76,10 @@ function Index() {
       return navTrans('user');
     }
 
+    if (location.pathname.includes('play')) {
+      return navTrans('play');
+    }
+
     return navTrans('overview');
   }, [location, navTrans]);
 
@@ -90,11 +94,11 @@ function Index() {
       label: navTrans('database'),
       onClick: () => navigate(`/databases/${database}/collections`),
     },
-    // {
-    //   icon: icons.navSearch,
-    //   label: navTrans('search'),
-    //   onClick: () => navigate('/search'),
-    // },
+    {
+      icon: icons.code,
+      label: navTrans('play'),
+      onClick: () => navigate('/play'),
+    },
   ];
 
   if (enableUser) {

+ 146 - 0
client/src/pages/play/Play.tsx

@@ -0,0 +1,146 @@
+import { EditorView, placeholder } from '@codemirror/view';
+import { Box, Paper, useTheme } from '@mui/material';
+import {
+  FC,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
+
+import CodeBlock from '@/components/code/CodeBlock';
+import { ATTU_PLAY_CODE } from '@/consts';
+import { authContext, dataContext } from '@/context';
+import { useNavigationHook } from '@/hooks';
+import { ALL_ROUTER_TYPES } from '@/router/consts';
+
+import { useCodeMirror } from './hooks/use-codemirror';
+import { Autocomplete } from './language/extensions/autocomplete';
+import { KeyMap } from './language/extensions/keymap';
+import { MilvusHTTP } from './language/milvus.http';
+import { getCMStyle, getStyles } from './style';
+import { CustomEventNameEnum, PlaygroundCustomEventDetail } from './Types';
+import { DocumentEventManager } from './utils/event';
+
+const Play: FC = () => {
+  // hooks
+  const theme = useTheme();
+  useNavigationHook(ALL_ROUTER_TYPES.PLAY);
+  const [detail, setDetail] = useState<PlaygroundCustomEventDetail>(
+    {} as PlaygroundCustomEventDetail
+  );
+  const { collections, databases, loading } = useContext(dataContext);
+  const { isManaged, authReq } = useContext(authContext);
+
+  // styles
+  const classes = getStyles();
+  const [code, setCode] = useState(() => {
+    const savedCode = localStorage.getItem(ATTU_PLAY_CODE);
+    return savedCode || '';
+  });
+
+  // refs
+  const container = useRef<HTMLDivElement>(null);
+
+  const content = detail.error
+    ? JSON.stringify(detail.error, null, 2)
+    : JSON.stringify(detail.response, null, 2);
+
+  const extensions = useMemo(() => {
+    const { address, token, username, password } = authReq;
+    const getBaseUrl = () => {
+      return address.startsWith('http') ? address : `http://${address}`;
+    };
+    return [
+      placeholder('Write your code here'),
+      EditorView.lineWrapping,
+      EditorView.theme(getCMStyle(theme)),
+      KeyMap(),
+      MilvusHTTP({
+        baseUrl: getBaseUrl(),
+        token,
+        username,
+        password,
+        isManaged,
+        isDarkMode: theme.palette.mode === 'dark',
+      }),
+      Autocomplete({ databases, collections }),
+    ];
+  }, [databases, collections, authReq, theme.palette.mode]);
+
+  const handleCodeChange = useCallback((code: string) => {
+    setCode(code);
+  }, []);
+
+  useCodeMirror({
+    container: container.current,
+    value: code,
+    extensions,
+    onChange: handleCodeChange,
+  });
+
+  // save code to local storage
+  useEffect(() => {
+    localStorage.setItem(ATTU_PLAY_CODE, code);
+  }, [code]);
+
+  useEffect(() => {
+    const handleCodeMirrorResponse = (event: Event) => {
+      const { detail } = event as CustomEvent<PlaygroundCustomEventDetail>;
+      setDetail(detail);
+    };
+
+    const unsubscribe = DocumentEventManager.subscribe(
+      CustomEventNameEnum.PlaygroundResponseDetail,
+      handleCodeMirrorResponse
+    );
+
+    return () => {
+      unsubscribe();
+    };
+  }, []);
+
+  const renderResponse = () => {
+    const style =
+      theme.palette.mode !== 'dark'
+        ? {
+            hljs: {
+              display: 'block',
+              overflowX: 'auto' as const,
+              padding: '12px 0',
+              color: '#333',
+              margin: 0,
+              background: theme.palette.background.paper,
+            },
+          }
+        : undefined;
+    return (
+      <CodeBlock
+        wrapperClass={classes.response}
+        language="json"
+        code={content || '{}'}
+        style={style}
+      />
+    );
+  };
+
+  return (
+    <Box className={classes.root}>
+      <Paper elevation={0} className={classes.leftPane}>
+        <div
+          ref={container}
+          defaultValue={code}
+          className={classes.editor}
+        ></div>
+      </Paper>
+
+      <Paper elevation={0} className={classes.rightPane}>
+        {renderResponse()}
+      </Paper>
+    </Box>
+  );
+};
+
+export default Play;

+ 34 - 0
client/src/pages/play/Types.ts

@@ -0,0 +1,34 @@
+import { type AxiosError, type AxiosResponse } from "axios"
+
+export enum CustomEventNameEnum {
+  PlaygroundResponseDetail = 'playgroundResponseDetail'
+}
+
+export interface PlaygroundCustomEventDetail {
+  loading?: boolean
+  response?: AxiosResponse['data']
+  error?: object
+}
+
+export interface PlaygroundExtensionParams {
+  baseUrl: string
+  isManaged?: boolean
+  token?: string
+  username?: string
+  password?: string
+  isDarkMode?: boolean
+}
+
+export type IdentifierMap = {
+  name: string;
+  required?: boolean;
+  type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
+  children?: IdentifierMap[];
+};
+
+export type CompletionMacro = {
+  label: string;
+  apply: string;
+  type: 'text';
+  detail: 'macro';
+}

+ 171 - 0
client/src/pages/play/hooks/use-codemirror.ts

@@ -0,0 +1,171 @@
+import {
+  EditorState,
+  Extension,
+  Annotation,
+  StateEffect,
+} from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+import { useState, useEffect, KeyboardEventHandler } from 'react';
+import {
+  lineNumbers,
+  highlightActiveLineGutter,
+} from '../language/extensions/gutter';
+import {
+  highlightSpecialChars,
+  drawSelection,
+  dropCursor,
+  rectangularSelection,
+  crosshairCursor,
+  highlightActiveLine,
+  keymap,
+} from '@codemirror/view';
+export { EditorView } from '@codemirror/view';
+import {
+  foldGutter,
+  indentOnInput,
+  syntaxHighlighting,
+  defaultHighlightStyle,
+  bracketMatching,
+  foldKeymap,
+} from '@codemirror/language';
+import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
+import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
+import {
+  closeBrackets,
+  autocompletion,
+  closeBracketsKeymap,
+  completionKeymap,
+} from '@codemirror/autocomplete';
+import { lintKeymap } from '@codemirror/lint';
+
+const basicSetup = () => [
+  lineNumbers(),
+  highlightActiveLineGutter(),
+  highlightSpecialChars(),
+  history(),
+  foldGutter(),
+  drawSelection(),
+  dropCursor(),
+  EditorState.allowMultipleSelections.of(true),
+  indentOnInput(),
+  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
+  bracketMatching(),
+  closeBrackets(),
+  autocompletion(),
+  rectangularSelection(),
+  crosshairCursor(),
+  highlightActiveLine(),
+  highlightSelectionMatches(),
+  keymap.of([
+    ...closeBracketsKeymap,
+    ...defaultKeymap,
+    ...searchKeymap,
+    ...historyKeymap,
+    ...foldKeymap,
+    ...completionKeymap,
+    ...lintKeymap,
+  ]),
+];
+
+const External = Annotation.define<boolean>();
+
+export interface UseCodeMirrorProps {
+  container?: HTMLDivElement | null;
+  value?: string;
+  extensions?: Extension[];
+  onChange?: (value: string) => void;
+}
+
+export const useCodeMirror = (props: UseCodeMirrorProps) => {
+  const { value, extensions, onChange } = props;
+
+  const [container, setContainer] = useState<HTMLDivElement | null>();
+  const [view, setView] = useState<EditorView>();
+
+  const updateListener = EditorView.updateListener.of(update => {
+    if (update.changes) {
+      onChange?.(update.state.doc.toString());
+    }
+  });
+
+  const getExtensions = () => {
+    return [updateListener, basicSetup(), ...(extensions ?? [])];
+  };
+
+  // init editor
+  useEffect(() => {
+    if (container && !view) {
+      const startState = EditorState.create({
+        doc: value,
+        extensions: getExtensions(),
+      });
+
+      const editorView = new EditorView({
+        state: startState,
+        parent: container,
+      });
+      setView(editorView);
+    }
+
+    return () => {};
+  }, [container]);
+
+  // update value
+  useEffect(() => {
+    if (value === undefined) {
+      return;
+    }
+
+    const currentValue = view?.state.doc.toString() ?? '';
+    if (view && value !== currentValue) {
+      view.dispatch({
+        changes: {
+          from: 0,
+          to: currentValue.length,
+          insert: value || '',
+        },
+        annotations: [External.of(true)],
+      });
+    }
+  }, [value]);
+
+  // update extensions
+  useEffect(() => {
+    if (view) {
+      view.dispatch({
+        effects: [StateEffect.reconfigure.of(getExtensions())],
+      });
+    }
+  }, [extensions, view]);
+
+  useEffect(() => setContainer(props.container), [props.container]);
+
+  // Handle codelens shortcuts
+  useEffect(() => {
+    const handler = (event: Event) => {
+      if (event instanceof KeyboardEvent) {
+        if (event.metaKey && event.shiftKey && event.key === 'Enter') {
+          const currentRunButton = document.querySelector(
+            '.milvus-http-request-highlight .playground-codelens .run-button'
+          );
+          currentRunButton?.dispatchEvent(new MouseEvent('click'));
+          event.preventDefault();
+        } else if (event.metaKey && event.key === 'h') {
+          const currentDocsButton = document.querySelector(
+            '.milvus-http-request-highlight .playground-codelens .docs-button'
+          );
+          currentDocsButton?.dispatchEvent(new MouseEvent('click'));
+          event.preventDefault();
+        }
+      }
+    };
+
+    container?.addEventListener('keydown', handler);
+
+    return () => {
+      container?.removeEventListener('keydown', handler);
+    };
+  }, [container]);
+
+  return { view };
+};

+ 323 - 0
client/src/pages/play/language/extensions/autocomplete.ts

@@ -0,0 +1,323 @@
+import {
+  autocompletion,
+  Completion,
+  CompletionContext,
+} from '@codemirror/autocomplete';
+import { EditorView } from 'codemirror';
+import { syntaxTree } from '@codemirror/language';
+import { EditorState, Text } from '@codemirror/state';
+import { CollectionObject, DatabaseObject } from '@server/types';
+import { VersionMap, ObjectMap, IdentifierMapArr } from './consts';
+import { CompletionMacro } from '../../Types';
+
+function getDefaultValue(
+  type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+) {
+  switch (type) {
+    case 'string':
+      return '';
+    case 'number':
+      return 0;
+    case 'boolean':
+      return false;
+    case 'object':
+      return {};
+    case 'array':
+      return [];
+    default:
+      return '';
+  }
+}
+
+function getMilvusMacros(): CompletionMacro[] {
+  const marcos: CompletionMacro[] = [];
+  IdentifierMapArr.forEach(obj => {
+    obj.children?.forEach(operation => {
+      const params = (operation.children ?? []).filter(item => item.required);
+      const body = params.reduce((acc, param) => {
+        acc[param.name] = getDefaultValue(param.type ?? 'string');
+        return acc;
+      }, {} as Record<string, any>);
+      marcos.push({
+        label: `${obj.name}:${operation.name}`,
+        type: 'text',
+        apply: `POST /v2/vectordb/${obj.name}/${operation.name}\n${
+          params.length > 0 ? JSON.stringify(body, null, 2) : ''
+        }`,
+        detail: 'macro',
+      });
+    });
+  });
+  return marcos;
+}
+
+function getContextStack(doc: Text, pos: number, startLine: number = 1) {
+  const stack = [];
+  let currentLine = doc.lineAt(pos).number;
+  let bracesCount = 0;
+  let bracketsCount = 0;
+
+  for (let lineNum = currentLine; lineNum >= startLine; lineNum--) {
+    const lineText =
+      lineNum === currentLine
+        ? doc.sliceString(doc.line(lineNum).from, pos)
+        : doc.line(lineNum).text;
+
+    for (let i = lineText.length - 1; i >= 0; i--) {
+      const char = lineText[i];
+
+      if (char === '}') {
+        bracesCount++;
+      } else if (char === '{') {
+        bracesCount--;
+        if (bracesCount < 0) {
+          const keyMatch = lineText.match(/"([^"]+)"\s*:\s*\{/);
+          if (keyMatch) {
+            stack.push(keyMatch[1]);
+          }
+          bracesCount = 0;
+        }
+      } else if (char === ']') {
+        bracketsCount++;
+      } else if (char === '[') {
+        bracketsCount--;
+        if (bracketsCount < 0) {
+          const keyMatch = lineText.match(/"([^"]+)"\s*:\s*\[/);
+          if (keyMatch) {
+            stack.push(keyMatch[1]);
+          }
+          bracketsCount = 0;
+        }
+      }
+    }
+
+    if (bracesCount < 0) {
+      break;
+    }
+  }
+
+  return stack;
+}
+
+function methodCompletions(context: CompletionContext) {
+  const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
+
+  const line = context.state.doc.lineAt(context.pos);
+  const textBefore = context.state.doc.sliceString(line.from, nodeBefore.from);
+  const isFirstWord = !textBefore.trim();
+
+  if (
+    nodeBefore.parent?.name === 'MultipleRequests' ||
+    (nodeBefore.parent?.name === 'Request' &&
+      (nodeBefore.name === 'HTTPMethod' || isFirstWord))
+  ) {
+    const word = context.matchBefore(/\w*/);
+    if (!word || (word.from === word.to && !context.explicit)) return null;
+    return {
+      from: word.from,
+      options: [
+        ...getMilvusMacros(),
+        { label: 'GET', type: 'keyword' },
+        { label: 'POST', type: 'keyword' },
+        { label: 'PUT', type: 'keyword' },
+        { label: 'DELETE', type: 'keyword' },
+        { label: 'TOKEN', type: 'keyword' },
+      ],
+    };
+  }
+  return null;
+}
+
+function urlCompletions(context: CompletionContext) {
+  const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
+
+  if (
+    ![
+      'Request',
+      'URL',
+      'API',
+      'Aliases',
+      'Collections',
+      'Databases',
+      'Partitions',
+      'Jobs',
+      'Indexes',
+      'ResourceGroups',
+      'Roles',
+      'Users',
+      'Vectors',
+    ].includes(nodeBefore.parent?.name ?? '')
+  ) {
+    return null;
+  }
+
+  const word = context.matchBefore(/\w*/);
+  const url = context.matchBefore(/[/\w]*/) ?? { text: '' };
+  const urlArr = url.text.split('/');
+
+  if (url?.text === '/') {
+    const keys = Object.keys(VersionMap);
+    return {
+      from: word?.from ?? 0,
+      options: keys.map(key => ({ label: key, type: 'keyword' })),
+    };
+  } else if (
+    Object.keys(VersionMap).some(item => item === urlArr[urlArr.length - 2])
+  ) {
+    const keys =
+      VersionMap[urlArr[urlArr.length - 2] as keyof typeof VersionMap];
+    return {
+      from: word?.from ?? 0,
+      options: keys.map(key => ({ label: key, type: 'keyword' })),
+    };
+  } else if (
+    Object.keys(ObjectMap).some(item => item === urlArr[urlArr.length - 2])
+  ) {
+    const keys = ObjectMap[urlArr[urlArr.length - 2] as keyof typeof ObjectMap];
+    return {
+      from: word?.from ?? 0,
+      options: keys.map(key => ({ label: key, type: 'keyword' })),
+    };
+  } else if (
+    IdentifierMapArr.some(item => item.name === urlArr[urlArr.length - 2])
+  ) {
+    const list =
+      IdentifierMapArr.find(item => item.name === urlArr[urlArr.length - 2])
+        ?.children ?? [];
+    return {
+      from: word?.from ?? 0,
+      options: list.map(item => ({ label: item.name, type: 'keyword' })),
+    };
+  }
+
+  return null;
+}
+
+function bodyIdentifierCompletions(context: CompletionContext) {
+  const state = context.state;
+  const doc = state.doc;
+  const tree = syntaxTree(state);
+  const cursor = state.selection.main.head;
+
+  let nodeAtCursor = tree.resolve(cursor, -1);
+  let requestNode = null;
+  while (nodeAtCursor) {
+    if (nodeAtCursor.name === 'Request') {
+      requestNode = nodeAtCursor;
+      break;
+    }
+    if (!nodeAtCursor.parent) {
+      break;
+    }
+    nodeAtCursor = nodeAtCursor.parent;
+  }
+
+  if (!requestNode) {
+    return null;
+  }
+
+  const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
+  if (nodeBefore.name !== 'Identifier') {
+    return null;
+  }
+
+  //map properties from body to cursor position
+  const bodyNode = requestNode.getChildren('Body')[0];
+  const bodyStartLine = doc.lineAt(bodyNode.from).number;
+  const properties = getContextStack(
+    state.doc,
+    context.pos,
+    bodyStartLine
+  ).reverse();
+
+  const urlNode = requestNode.getChildren('URL')[0];
+  const url = urlNode ? doc.sliceString(urlNode.from, urlNode.to) : '';
+  const urlArr = url.split('/');
+  const objectName = urlArr[urlArr.length - 2];
+  const funcName = urlArr[urlArr.length - 1];
+
+  let identifiers =
+    IdentifierMapArr.find(item => item.name === objectName)?.children?.find(
+      item => item.name === funcName
+    )?.children ?? [];
+
+  if (properties.length > 0) {
+    for (let i = 0; i < properties.length; i++) {
+      const identifier = identifiers.find(item => item.name === properties[i]);
+      if (identifier) {
+        identifiers = identifier.children ?? [];
+      }
+    }
+  }
+
+  const word = context.matchBefore(/\w*/);
+  return {
+    from: word?.from ?? 0,
+    options: identifiers.map(item => ({ label: item.name, type: 'keyword' })),
+  };
+}
+
+function getBodyValueCompletions(options: {
+  databases: DatabaseObject[];
+  collections: CollectionObject[];
+}) {
+  return (context: CompletionContext) => {
+    const { databases, collections } = options;
+    const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
+    const line = context.matchBefore(/.*/) ?? { text: '' };
+    const identifier = line.text.match(/"([^"]+)"\s*:\s*/)?.[1];
+
+    if (!identifier || nodeBefore.parent?.name !== 'Value') {
+      return null;
+    }
+
+    const word = context.matchBefore(/\w*/);
+
+    if (identifier === 'dbName') {
+      return {
+        from: word?.from ?? 0,
+        options: databases.map(db => ({ label: db.db_name, type: 'keyword' })),
+      };
+    }
+
+    if (identifier === 'collectionName') {
+      return {
+        from: word?.from ?? 0,
+        options: collections.map(col => ({
+          label: col.collection_name,
+          type: 'keyword',
+        })),
+      };
+    }
+
+    return null;
+  };
+}
+
+function addTabBadge() {
+  return {
+    render: (completion: Completion, state: EditorState, view: EditorView) => {
+      const span = document.createElement('span');
+      span.className = 'cm-autocomplete-option-tab-badge';
+      span.textContent = 'tab';
+      return span;
+    },
+    position: 100,
+  };
+}
+
+export function Autocomplete(options: {
+  databases: DatabaseObject[];
+  collections: CollectionObject[];
+}) {
+  return autocompletion({
+    override: [
+      methodCompletions,
+      urlCompletions,
+      bodyIdentifierCompletions,
+      getBodyValueCompletions(options),
+    ],
+    addToOptions: [addTabBadge()],
+    icons: false,
+  });
+}

+ 124 - 0
client/src/pages/play/language/extensions/autocompletion.ts

@@ -0,0 +1,124 @@
+import {
+  EditorView,
+  Decoration,
+  DecorationSet,
+  keymap,
+  WidgetType,
+} from '@codemirror/view';
+import { StateEffect, StateField } from '@codemirror/state';
+import { insertTab } from '@codemirror/commands';
+
+// Use ReturnType<typeof setTimeout> to get the correct type for the timeout
+type Timeout = ReturnType<typeof setTimeout>;
+
+// Define a custom widget by extending WidgetType
+class GhostTextWidget extends WidgetType {
+  constructor(private text: string) {
+    super();
+  }
+
+  eq(other: GhostTextWidget): boolean {
+    return this.text === other.text;
+  }
+
+  toDOM(): HTMLElement {
+    const span = document.createElement('span');
+    span.textContent = this.text;
+    span.style.color = '#999';
+    span.style.fontStyle = 'italic';
+    return span;
+  }
+}
+
+let timeout: Timeout | null = null; // Change the type to Timeout | null
+let ghostText: { from: number; to: number; text: string } | null = null;
+
+const addGhostText =
+  StateEffect.define<{ from: number; to: number; text: string }>();
+const removeGhostText = StateEffect.define<DecorationSet>();
+
+// Create a StateField to store the ghost text
+const ghostTextField = StateField.define({
+  create() {
+    return Decoration.none; // Start with no decorations
+  },
+  update(value, tr) {
+    // Start with no decorations
+    for (const effect of tr.effects) {
+      if (effect.is(addGhostText)) {
+        const { from, to, text } = effect.value;
+        const widget = Decoration.widget({
+          widget: new GhostTextWidget(text),
+          side: 1,
+        });
+        // Return a DecorationSet with the new widget
+        return Decoration.set(widget.range(from));
+      } else if (effect.is(removeGhostText)) {
+        return Decoration.none; // Clear all decorations
+      }
+    }
+    return value;
+  },
+});
+
+// Define the autocomplete extension
+export const tabCompletion = [
+  EditorView.updateListener.of(update => {
+    if (update.docChanged) {
+      if (timeout) clearTimeout(timeout);
+      if (ghostText) {
+        update.view.dispatch({
+          effects: removeGhostText.of(Decoration.none),
+        });
+        ghostText = null;
+      }
+
+      timeout = setTimeout(() => {
+        const cursorPos = update.state.selection.main.head;
+        const line = update.state.doc.lineAt(cursorPos);
+        const textBeforeCursor = line.text.slice(0, cursorPos - line.from);
+
+        if (textBeforeCursor.endsWith('fun')) {
+          ghostText = {
+            from: cursorPos,
+            to: cursorPos,
+            text: 'ction',
+          };
+
+          update.view.dispatch({
+            effects: addGhostText.of(ghostText),
+          });
+        }
+      }, 300);
+    }
+  }),
+  ghostTextField,
+  EditorView.decorations.compute([ghostTextField], state => {
+    return state.field(ghostTextField);
+  }),
+  keymap.of([
+    {
+      key: 'Tab',
+      run: view => {
+        if (ghostText) {
+          view.dispatch({
+            changes: {
+              from: ghostText.from,
+              to: ghostText.to,
+              insert: ghostText.text,
+            },
+            selection: {
+              anchor: ghostText.from + ghostText.text.length,
+              head: ghostText.from + ghostText.text.length,
+            },
+            effects: removeGhostText.of(Decoration.none),
+          });
+          ghostText = null;
+          return true;
+        } else {
+          return insertTab(view);
+        }
+      },
+    },
+  ]),
+];

+ 185 - 0
client/src/pages/play/language/extensions/codelens.ts

@@ -0,0 +1,185 @@
+import { syntaxTree } from '@codemirror/language';
+import { Range, Text } from '@codemirror/state';
+import { ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view';
+import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
+import { AxiosError } from 'axios';
+import { MILVUS_RESTFUL_DOC_URL, CLOUD_RESTFUL_DOC_URL } from '@/consts';
+import { CustomEventNameEnum, PlaygroundExtensionParams } from '../../Types';
+import { createPlaygroundRequest, DocumentEventManager } from '../../utils';
+
+const apiPlaygroundRequest = createPlaygroundRequest('backend');
+
+class CodeLensWidget extends WidgetType {
+  constructor(
+    readonly options: {
+      readonly line: number;
+      readonly onRunClick: () => void;
+      readonly onDocsClick: () => void;
+    }
+  ) {
+    super();
+  }
+
+  toDOM(view: EditorView): HTMLElement {
+    const container = document.createElement('div');
+    container.className = 'playground-codelens';
+
+    const runBtn = document.createElement('div');
+    runBtn.className = 'codelens-item run-button';
+    runBtn.textContent = 'RUN';
+    runBtn.title = `⌘ + ⇧ + ↵`;
+    runBtn.onclick = this.options.onRunClick;
+
+    const docsBtn = document.createElement('a');
+    docsBtn.className = 'codelens-item docs-button';
+    docsBtn.textContent = 'DOCS';
+    docsBtn.title = `⌘ + H`;
+    docsBtn.onclick = this.options.onDocsClick;
+
+    container.appendChild(runBtn);
+    container.appendChild(docsBtn);
+    return container;
+  }
+}
+
+const getRequestParams = (
+  requestNode: ReturnType<ReturnType<typeof syntaxTree>['resolve']>,
+  doc: Text,
+  options: PlaygroundExtensionParams
+) => {
+  const urlNode = requestNode.getChildren('URL')[0];
+  const bodyNode = requestNode.getChildren('Body')[0];
+  const HTTPMethodNode = requestNode.getChildren('HTTPMethod')[0];
+
+  const url = urlNode ? doc.sliceString(urlNode.from, urlNode.to) : '';
+  const body = bodyNode ? doc.sliceString(bodyNode.from, bodyNode.to) : '{}';
+  const method = HTTPMethodNode
+    ? doc.sliceString(HTTPMethodNode.from, HTTPMethodNode.to)
+    : '';
+  let bodyObj: object = {};
+  try {
+    bodyObj = JSON.parse(body);
+  } catch (err) {
+    console.error('Failed to parse body: ', (err as Error).message);
+  }
+  const host = options.baseUrl;
+
+  const getAuthorization = () => {
+    if (options.token) {
+      return `Bearer ${options.token}`;
+    }
+    if (options.username && options.password) {
+      return `Bearer ${options.username}:${options.password}`;
+    }
+    return undefined;
+  };
+
+  return {
+    host,
+    url,
+    headers: {
+      Authorization: getAuthorization(),
+    },
+    method,
+    body: bodyObj,
+  };
+};
+
+export const codeLensDecoration = (options: PlaygroundExtensionParams) =>
+  ViewPlugin.fromClass(
+    class {
+      decorations: DecorationSet;
+
+      constructor(view: EditorView) {
+        this.decorations = this.updateDecorations(view);
+      }
+
+      update(update: ViewUpdate) {
+        if (update.docChanged || update.viewportChanged) {
+          this.decorations = this.updateDecorations(update.view);
+        }
+      }
+
+      updateDecorations(view: EditorView): DecorationSet {
+        const widgets: Range<Decoration>[] = [];
+        const tree = syntaxTree(view.state);
+        const state = view.state;
+        const doc = state.doc;
+
+        let token = '';
+        // Find the Authorization node at the root level
+        tree.iterate({
+          enter: node => {
+            if (node.name === 'Authorization') {
+              const authorizationNode = node.node;
+              const tokenNode =
+                authorizationNode.getChildren('UnquotedString')[0];
+              token = tokenNode
+                ? doc.sliceString(tokenNode.from, tokenNode.to)
+                : '';
+              return false;
+            }
+          },
+        });
+
+        tree.iterate({
+          enter: node => {
+            if (node.type.name === 'Request' || node.name === 'Request') {
+              const line = view.state.doc.lineAt(node.from).number - 1;
+
+              const requestNode = node.node;
+              if (token) {
+                options.token = token;
+              }
+              const params = getRequestParams(requestNode, doc, options);
+
+              const onRunClick = async () => {
+                try {
+                  DocumentEventManager.dispatch(
+                    CustomEventNameEnum.PlaygroundResponseDetail,
+                    { loading: true, response: 'running' }
+                  );
+                  const res = await apiPlaygroundRequest(params);
+                  DocumentEventManager.dispatch(
+                    CustomEventNameEnum.PlaygroundResponseDetail,
+                    { response: res.data, loading: false }
+                  );
+                } catch (err) {
+                  DocumentEventManager.dispatch(
+                    CustomEventNameEnum.PlaygroundResponseDetail,
+                    {
+                      loading: false,
+                      error: (err as AxiosError).response?.data ?? {
+                        message: (err as Error).message,
+                      },
+                    }
+                  );
+                  console.error('Error:', err);
+                }
+              };
+
+              const onDocsClick = () => {
+                const { isManaged } = options;
+                window.open(
+                  isManaged ? CLOUD_RESTFUL_DOC_URL : MILVUS_RESTFUL_DOC_URL,
+                  '_blank'
+                );
+              };
+
+              const widget = Decoration.widget({
+                widget: new CodeLensWidget({ line, onRunClick, onDocsClick }),
+                side: -1,
+              });
+
+              widgets.push(widget.range(view.state.doc.line(line + 1).from));
+            }
+          },
+        });
+
+        return Decoration.set(widgets);
+      }
+    },
+    {
+      decorations: v => v.decorations,
+    }
+  );

+ 23 - 0
client/src/pages/play/language/extensions/consts.ts

@@ -0,0 +1,23 @@
+import { IdentifierMap } from '../../Types';
+import OpenApiJSON from './json/openapi.json';
+
+export const VersionMap = {
+  v2: ['vectordb'],
+};
+
+export const ObjectMap = {
+  vectordb: [
+    'aliases',
+    'databases',
+    'collections',
+    'partitions',
+    'jobs',
+    'indexes',
+    'resource_groups',
+    'roles',
+    'users',
+    'entities',
+  ],
+};
+
+export const IdentifierMapArr = OpenApiJSON as IdentifierMap[];

+ 690 - 0
client/src/pages/play/language/extensions/gutter.ts

@@ -0,0 +1,690 @@
+import {
+  combineConfig,
+  MapMode,
+  Facet,
+  Extension,
+  EditorState,
+  RangeValue,
+  RangeSet,
+  RangeCursor,
+} from '@codemirror/state';
+import {
+  BlockInfo,
+  BlockType,
+  Direction,
+  ViewPlugin,
+  ViewUpdate,
+  WidgetType,
+} from '@codemirror/view';
+import { EditorView } from 'codemirror';
+
+/// A gutter marker represents a bit of information attached to a line
+/// in a specific gutter. Your own custom markers have to extend this
+/// class.
+export abstract class GutterMarker extends RangeValue {
+  /// @internal
+  compare(other: GutterMarker) {
+    return (
+      this == other || (this.constructor == other.constructor && this.eq(other))
+    );
+  }
+
+  /// Compare this marker to another marker of the same type.
+  eq(other: GutterMarker): boolean {
+    return false;
+  }
+
+  /// Render the DOM node for this marker, if any.
+  toDOM?(view: EditorView): Node;
+
+  /// This property can be used to add CSS classes to the gutter
+  /// element that contains this marker.
+  elementClass!: string;
+
+  /// Called if the marker has a `toDOM` method and its representation
+  /// was removed from a gutter.
+  destroy(dom: Node) {}
+}
+
+GutterMarker.prototype.elementClass = '';
+GutterMarker.prototype.toDOM = undefined;
+GutterMarker.prototype.mapMode = MapMode.TrackBefore;
+GutterMarker.prototype.startSide = GutterMarker.prototype.endSide = -1;
+GutterMarker.prototype.point = true;
+
+/// Facet used to add a class to all gutter elements for a given line.
+/// Markers given to this facet should _only_ define an
+/// [`elementclass`](#view.GutterMarker.elementClass), not a
+/// [`toDOM`](#view.GutterMarker.toDOM) (or the marker will appear
+/// in all gutters for the line).
+export const gutterLineClass = Facet.define<RangeSet<GutterMarker>>();
+
+/// Facet used to add a class to all gutter elements next to a widget.
+/// Should not provide widgets with a `toDOM` method.
+export const gutterWidgetClass =
+  Facet.define<
+    (
+      view: EditorView,
+      widget: WidgetType,
+      block: BlockInfo
+    ) => GutterMarker | null
+  >();
+
+type Handlers = {
+  [event: string]: (view: EditorView, line: BlockInfo, event: Event) => boolean;
+};
+
+interface GutterConfig {
+  /// An extra CSS class to be added to the wrapper (`cm-gutter`)
+  /// element.
+  class?: string;
+  /// Controls whether empty gutter elements should be rendered.
+  /// Defaults to false.
+  renderEmptyElements?: boolean;
+  /// Retrieve a set of markers to use in this gutter.
+  markers?: (
+    view: EditorView
+  ) => RangeSet<GutterMarker> | readonly RangeSet<GutterMarker>[];
+  /// Can be used to optionally add a single marker to every line.
+  lineMarker?: (
+    view: EditorView,
+    line: BlockInfo,
+    otherMarkers: readonly GutterMarker[]
+  ) => GutterMarker | null;
+  /// Associate markers with block widgets in the document.
+  widgetMarker?: (
+    view: EditorView,
+    widget: WidgetType,
+    block: BlockInfo
+  ) => GutterMarker | null;
+  /// If line or widget markers depend on additional state, and should
+  /// be updated when that changes, pass a predicate here that checks
+  /// whether a given view update might change the line markers.
+  lineMarkerChange?: null | ((update: ViewUpdate) => boolean);
+  /// Add a hidden spacer element that gives the gutter its base
+  /// width.
+  initialSpacer?: null | ((view: EditorView) => GutterMarker);
+  /// Update the spacer element when the view is updated.
+  updateSpacer?:
+    | null
+    | ((spacer: GutterMarker, update: ViewUpdate) => GutterMarker);
+  /// Supply event handlers for DOM events on this gutter.
+  domEventHandlers?: Handlers;
+}
+
+const defaults = {
+  class: '',
+  renderEmptyElements: false,
+  elementStyle: '',
+  markers: () => RangeSet.empty,
+  lineMarker: () => null,
+  widgetMarker: () => null,
+  lineMarkerChange: null,
+  initialSpacer: null,
+  updateSpacer: null,
+  domEventHandlers: {},
+};
+
+const activeGutters = Facet.define<Required<GutterConfig>>();
+
+/// Define an editor gutter. The order in which the gutters appear is
+/// determined by their extension priority.
+export function gutter(config: GutterConfig): Extension {
+  return [gutters(), activeGutters.of({ ...defaults, ...config })];
+}
+
+const unfixGutters = Facet.define<boolean, boolean>({
+  combine: values => values.some(x => x),
+});
+
+/// The gutter-drawing plugin is automatically enabled when you add a
+/// gutter, but you can use this function to explicitly configure it.
+///
+/// Unless `fixed` is explicitly set to `false`, the gutters are
+/// fixed, meaning they don't scroll along with the content
+/// horizontally (except on Internet Explorer, which doesn't support
+/// CSS [`position:
+/// sticky`](https://developer.mozilla.org/en-US/docs/Web/CSS/position#sticky)).
+export function gutters(config?: { fixed?: boolean }): Extension {
+  let result: Extension[] = [gutterView];
+  if (config && config.fixed === false) result.push(unfixGutters.of(true));
+  return result;
+}
+
+const gutterView = ViewPlugin.fromClass(
+  class {
+    gutters: SingleGutterView[];
+    dom: HTMLElement;
+    fixed: boolean;
+    prevViewport: { from: number; to: number };
+
+    constructor(readonly view: EditorView) {
+      this.prevViewport = view.viewport;
+      this.dom = document.createElement('div');
+      this.dom.className = 'cm-gutters';
+      this.dom.setAttribute('aria-hidden', 'true');
+      this.dom.style.minHeight =
+        this.view.contentHeight / this.view.scaleY + 'px';
+      this.gutters = view.state
+        .facet(activeGutters)
+        .map(conf => new SingleGutterView(view, conf));
+      for (let gutter of this.gutters) this.dom.appendChild(gutter.dom);
+      this.fixed = !view.state.facet(unfixGutters);
+      if (this.fixed) {
+        // FIXME IE11 fallback, which doesn't support position: sticky,
+        // by using position: relative + event handlers that realign the
+        // gutter (or just force fixed=false on IE11?)
+        this.dom.style.position = 'sticky';
+      }
+      this.syncGutters(false);
+      view.scrollDOM.insertBefore(this.dom, view.contentDOM);
+    }
+
+    update(update: ViewUpdate) {
+      if (this.updateGutters(update)) {
+        // Detach during sync when the viewport changed significantly
+        // (such as during scrolling), since for large updates that is
+        // faster.
+        let vpA = this.prevViewport,
+          vpB = update.view.viewport;
+        let vpOverlap = Math.min(vpA.to, vpB.to) - Math.max(vpA.from, vpB.from);
+        this.syncGutters(vpOverlap < (vpB.to - vpB.from) * 0.8);
+      }
+      if (update.geometryChanged) {
+        this.dom.style.minHeight =
+          this.view.contentHeight / this.view.scaleY + 'px';
+      }
+      if (this.view.state.facet(unfixGutters) != !this.fixed) {
+        this.fixed = !this.fixed;
+        this.dom.style.position = this.fixed ? 'sticky' : '';
+      }
+      this.prevViewport = update.view.viewport;
+    }
+
+    syncGutters(detach: boolean) {
+      let after = this.dom.nextSibling;
+      if (detach) this.dom.remove();
+      let lineClasses = RangeSet.iter(
+        this.view.state.facet(gutterLineClass),
+        this.view.viewport.from
+      );
+      let classSet: GutterMarker[] = [];
+      let contexts = this.gutters.map(
+        gutter =>
+          new UpdateContext(
+            gutter,
+            this.view.viewport,
+            -this.view.documentPadding.top
+          )
+      );
+      for (let line of this.view.viewportLineBlocks) {
+        if (classSet.length) classSet = [];
+        if (Array.isArray(line.type)) {
+          let first = true;
+          for (let b of line.type) {
+            if (b.type == BlockType.Text && first) {
+              advanceCursor(lineClasses, classSet, b.from);
+              for (let cx of contexts) cx.line(this.view, b, classSet);
+              first = false;
+            } else if (b.widget) {
+              for (let cx of contexts) cx.widget(this.view, b);
+            }
+          }
+        } else if (line.type == BlockType.Text) {
+          advanceCursor(lineClasses, classSet, line.from);
+          for (let cx of contexts) cx.line(this.view, line, classSet);
+        } else if (line.widget) {
+          for (let cx of contexts) cx.widget(this.view, line);
+        }
+      }
+      for (let cx of contexts) cx.finish();
+      if (detach) this.view.scrollDOM.insertBefore(this.dom, after);
+    }
+
+    updateGutters(update: ViewUpdate) {
+      let prev = update.startState.facet(activeGutters),
+        cur = update.state.facet(activeGutters);
+      let change =
+        update.docChanged ||
+        update.heightChanged ||
+        update.viewportChanged ||
+        !RangeSet.eq(
+          update.startState.facet(gutterLineClass),
+          update.state.facet(gutterLineClass),
+          update.view.viewport.from,
+          update.view.viewport.to
+        );
+      if (prev == cur) {
+        for (let gutter of this.gutters)
+          if (gutter.update(update)) change = true;
+      } else {
+        change = true;
+        let gutters = [];
+        for (let conf of cur) {
+          let known = prev.indexOf(conf);
+          if (known < 0) {
+            gutters.push(new SingleGutterView(this.view, conf));
+          } else {
+            this.gutters[known].update(update);
+            gutters.push(this.gutters[known]);
+          }
+        }
+        for (let g of this.gutters) {
+          g.dom.remove();
+          if (gutters.indexOf(g) < 0) g.destroy();
+        }
+        for (let g of gutters) this.dom.appendChild(g.dom);
+        this.gutters = gutters;
+      }
+      return change;
+    }
+
+    destroy() {
+      for (let view of this.gutters) view.destroy();
+      this.dom.remove();
+    }
+  },
+  {
+    provide: plugin =>
+      EditorView.scrollMargins.of(view => {
+        let value = view.plugin(plugin);
+        if (!value || value.gutters.length == 0 || !value.fixed) return null;
+        return view.textDirection == Direction.LTR
+          ? { left: value.dom.offsetWidth * view.scaleX }
+          : { right: value.dom.offsetWidth * view.scaleX };
+      }),
+  }
+);
+
+function asArray<T>(val: T | readonly T[]) {
+  return (Array.isArray(val) ? val : [val]) as readonly T[];
+}
+
+function advanceCursor(
+  cursor: RangeCursor<GutterMarker>,
+  collect: GutterMarker[],
+  pos: number
+) {
+  while (cursor.value && cursor.from <= pos) {
+    if (cursor.from == pos) collect.push(cursor.value);
+    cursor.next();
+  }
+}
+
+class UpdateContext {
+  cursor: RangeCursor<GutterMarker>;
+  i = 0;
+
+  constructor(
+    readonly gutter: SingleGutterView,
+    viewport: { from: number; to: number },
+    public height: number
+  ) {
+    this.cursor = RangeSet.iter(gutter.markers, viewport.from);
+  }
+
+  addElement(
+    view: EditorView,
+    block: BlockInfo,
+    markers: readonly GutterMarker[]
+  ) {
+    let { gutter } = this,
+      above = (block.top - this.height) / view.scaleY,
+      height = block.height / view.scaleY;
+    if (this.i == gutter.elements.length) {
+      let newElt = new GutterElement(view, height, above, markers);
+      gutter.elements.push(newElt);
+      gutter.dom.appendChild(newElt.dom);
+    } else {
+      gutter.elements[this.i].update(view, height, above, markers);
+    }
+    this.height = block.bottom;
+    this.i++;
+  }
+
+  line(
+    view: EditorView,
+    line: BlockInfo,
+    extraMarkers: readonly GutterMarker[]
+  ) {
+    let localMarkers: GutterMarker[] = [];
+    advanceCursor(this.cursor, localMarkers, line.from);
+    if (extraMarkers.length) localMarkers = localMarkers.concat(extraMarkers);
+    let forLine = this.gutter.config.lineMarker(view, line, localMarkers);
+    if (forLine) localMarkers.unshift(forLine);
+
+    let gutter = this.gutter;
+    if (localMarkers.length == 0 && !gutter.config.renderEmptyElements) return;
+    this.addElement(view, line, localMarkers);
+  }
+
+  widget(view: EditorView, block: BlockInfo) {
+    let marker = this.gutter.config.widgetMarker(view, block.widget!, block),
+      markers = marker ? [marker] : null;
+    for (let cls of view.state.facet(gutterWidgetClass)) {
+      let marker = cls(view, block.widget!, block);
+      if (marker) (markers || (markers = [])).push(marker);
+    }
+    if (markers) this.addElement(view, block, markers);
+  }
+
+  finish() {
+    let gutter = this.gutter;
+    while (gutter.elements.length > this.i) {
+      let last = gutter.elements.pop()!;
+      gutter.dom.removeChild(last.dom);
+      last.destroy();
+    }
+  }
+}
+
+class SingleGutterView {
+  dom: HTMLElement;
+  elements: GutterElement[] = [];
+  markers: readonly RangeSet<GutterMarker>[];
+  spacer: GutterElement | null = null;
+
+  constructor(public view: EditorView, public config: Required<GutterConfig>) {
+    this.dom = document.createElement('div');
+    this.dom.className =
+      'cm-gutter' + (this.config.class ? ' ' + this.config.class : '');
+    for (let prop in config.domEventHandlers) {
+      this.dom.addEventListener(prop, (event: Event) => {
+        let target = event.target as HTMLElement,
+          y;
+        if (target != this.dom && this.dom.contains(target)) {
+          while (target.parentNode != this.dom)
+            target = target.parentNode as HTMLElement;
+          let rect = target.getBoundingClientRect();
+          y = (rect.top + rect.bottom) / 2;
+        } else {
+          y = (event as MouseEvent).clientY;
+        }
+        let line = view.lineBlockAtHeight(y - view.documentTop);
+        if (config.domEventHandlers[prop](view, line, event))
+          event.preventDefault();
+      });
+    }
+    this.markers = asArray(config.markers(view));
+    if (config.initialSpacer) {
+      this.spacer = new GutterElement(view, 0, 0, [config.initialSpacer(view)]);
+      this.dom.appendChild(this.spacer.dom);
+      this.spacer.dom.style.cssText +=
+        'visibility: hidden; pointer-events: none';
+    }
+  }
+
+  update(update: ViewUpdate) {
+    let prevMarkers = this.markers;
+    this.markers = asArray(this.config.markers(update.view));
+    if (this.spacer && this.config.updateSpacer) {
+      let updated = this.config.updateSpacer(this.spacer.markers[0], update);
+      if (updated != this.spacer.markers[0])
+        this.spacer.update(update.view, 0, 0, [updated]);
+    }
+    let vp = update.view.viewport;
+    return (
+      !RangeSet.eq(this.markers, prevMarkers, vp.from, vp.to) ||
+      (this.config.lineMarkerChange
+        ? this.config.lineMarkerChange(update)
+        : false)
+    );
+  }
+
+  destroy() {
+    for (let elt of this.elements) elt.destroy();
+  }
+}
+
+class GutterElement {
+  dom: HTMLElement;
+  height: number = -1;
+  above: number = 0;
+  markers: readonly GutterMarker[] = [];
+
+  constructor(
+    view: EditorView,
+    height: number,
+    above: number,
+    markers: readonly GutterMarker[]
+  ) {
+    this.dom = document.createElement('div');
+    this.dom.className = 'cm-gutterElement';
+    this.update(view, height, above, markers);
+  }
+
+  customMarkers = (markers: readonly GutterMarker[], view: EditorView) => {
+    for (let marker of markers) {
+      if (marker instanceof NumberMarker) {
+        try {
+          const text = view.state.doc.line(Number(marker.number)).text.trim();
+          if (
+            text.startsWith('POST') ||
+            text.startsWith('GET') ||
+            text.startsWith('PUT') ||
+            text.startsWith('DELETE')
+          ) {
+            this.dom.classList.add('cm-codelens-marker');
+          } else {
+            this.dom.classList.remove('cm-codelens-marker');
+          }
+        } catch (err) {}
+      }
+    }
+  };
+
+  update(
+    view: EditorView,
+    height: number,
+    above: number,
+    markers: readonly GutterMarker[]
+  ) {
+    if (this.height != height) {
+      this.height = height;
+      this.dom.style.height = height + 'px';
+    }
+    if (this.above != above)
+      this.dom.style.marginTop = (this.above = above) ? above + 'px' : '';
+
+    this.customMarkers(markers, view);
+
+    if (!sameMarkers(this.markers, markers)) this.setMarkers(view, markers);
+  }
+
+  setMarkers(view: EditorView, markers: readonly GutterMarker[]) {
+    let cls = 'cm-gutterElement',
+      domPos = this.dom.firstChild;
+    for (let iNew = 0, iOld = 0; ; ) {
+      let skipTo = iOld,
+        marker = iNew < markers.length ? markers[iNew++] : null,
+        matched = false;
+      if (marker) {
+        let c = marker.elementClass;
+        if (c) cls += ' ' + c;
+        for (let i = iOld; i < this.markers.length; i++)
+          if (this.markers[i].compare(marker)) {
+            skipTo = i;
+            matched = true;
+            break;
+          }
+      } else {
+        skipTo = this.markers.length;
+      }
+      while (iOld < skipTo) {
+        let next = this.markers[iOld++];
+        if (next.toDOM) {
+          next.destroy(domPos!);
+          let after = domPos!.nextSibling;
+          domPos!.remove();
+          domPos = after;
+        }
+      }
+      if (!marker) break;
+      if (marker.toDOM) {
+        if (matched) domPos = domPos!.nextSibling;
+        else this.dom.insertBefore(marker.toDOM(view), domPos);
+      }
+      if (matched) iOld++;
+    }
+    this.dom.className = cls;
+
+    this.customMarkers(markers, view);
+
+    this.markers = markers;
+  }
+
+  destroy() {
+    this.setMarkers(null as any, []); // First argument not used unless creating markers
+  }
+}
+
+function sameMarkers(
+  a: readonly GutterMarker[],
+  b: readonly GutterMarker[]
+): boolean {
+  if (a.length != b.length) return false;
+  for (let i = 0; i < a.length; i++) if (!a[i].compare(b[i])) return false;
+  return true;
+}
+
+interface LineNumberConfig {
+  /// How to display line numbers. Defaults to simply converting them
+  /// to string.
+  formatNumber?: (lineNo: number, state: EditorState) => string;
+  /// Supply event handlers for DOM events on this gutter.
+  domEventHandlers?: Handlers;
+}
+
+/// Facet used to provide markers to the line number gutter.
+export const lineNumberMarkers = Facet.define<RangeSet<GutterMarker>>();
+
+/// Facet used to create markers in the line number gutter next to widgets.
+export const lineNumberWidgetMarker =
+  Facet.define<
+    (
+      view: EditorView,
+      widget: WidgetType,
+      block: BlockInfo
+    ) => GutterMarker | null
+  >();
+
+const lineNumberConfig = Facet.define<
+  LineNumberConfig,
+  Required<LineNumberConfig>
+>({
+  combine(values) {
+    return combineConfig<Required<LineNumberConfig>>(
+      values,
+      { formatNumber: String, domEventHandlers: {} },
+      {
+        domEventHandlers(a: Handlers, b: Handlers) {
+          let result: Handlers = Object.assign({}, a);
+          for (let event in b) {
+            let exists = result[event],
+              add = b[event];
+            result[event] = exists
+              ? (view, line, event) =>
+                  exists(view, line, event) || add(view, line, event)
+              : add;
+          }
+          return result;
+        },
+      }
+    );
+  },
+});
+
+class NumberMarker extends GutterMarker {
+  constructor(readonly number: string) {
+    super();
+  }
+
+  eq(other: NumberMarker) {
+    return this.number == other.number;
+  }
+
+  toDOM() {
+    return document.createTextNode(this.number);
+  }
+}
+
+function formatNumber(view: EditorView, number: number) {
+  return view.state.facet(lineNumberConfig).formatNumber(number, view.state);
+}
+
+const lineNumberGutter = activeGutters.compute([lineNumberConfig], state => ({
+  class: 'cm-lineNumbers',
+  renderEmptyElements: false,
+  markers(view: EditorView) {
+    return view.state.facet(lineNumberMarkers);
+  },
+  lineMarker(view, line, others) {
+    if (others.some(m => m.toDOM)) return null;
+    return new NumberMarker(
+      formatNumber(view, view.state.doc.lineAt(line.from).number)
+    );
+  },
+  widgetMarker: (view, widget, block) => {
+    for (let m of view.state.facet(lineNumberWidgetMarker)) {
+      let result = m(view, widget, block);
+      if (result) return result;
+    }
+    return null;
+  },
+  lineMarkerChange: update =>
+    update.startState.facet(lineNumberConfig) !=
+    update.state.facet(lineNumberConfig),
+  initialSpacer(view: EditorView) {
+    return new NumberMarker(
+      formatNumber(view, maxLineNumber(view.state.doc.lines))
+    );
+  },
+  updateSpacer(spacer: GutterMarker, update: ViewUpdate) {
+    let max = formatNumber(
+      update.view,
+      maxLineNumber(update.view.state.doc.lines)
+    );
+    return max == (spacer as NumberMarker).number
+      ? spacer
+      : new NumberMarker(max);
+  },
+  domEventHandlers: state.facet(lineNumberConfig).domEventHandlers,
+}));
+
+/// Create a line number gutter extension.
+export function lineNumbers(config: LineNumberConfig = {}): Extension {
+  return [lineNumberConfig.of(config), gutters(), lineNumberGutter];
+}
+
+function maxLineNumber(lines: number) {
+  let last = 9;
+  while (last < lines) last = last * 10 + 9;
+  return last;
+}
+
+const activeLineGutterMarker = new (class extends GutterMarker {
+  elementClass = 'cm-activeLineGutter';
+})();
+
+const activeLineGutterHighlighter = gutterLineClass.compute(
+  ['selection'],
+  state => {
+    let marks = [],
+      last = -1;
+    for (let range of state.selection.ranges) {
+      let linePos = state.doc.lineAt(range.head).from;
+      if (linePos > last) {
+        last = linePos;
+        marks.push(activeLineGutterMarker.range(linePos));
+      }
+    }
+    return RangeSet.of(marks);
+  }
+);
+
+/// Returns an extension that adds a `cm-activeLineGutter` class to
+/// all gutter elements on the [active
+/// line](#view.highlightActiveLine).
+export function highlightActiveLineGutter() {
+  return activeLineGutterHighlighter;
+}

+ 19 - 0
client/src/pages/play/language/extensions/highlights.ts

@@ -0,0 +1,19 @@
+import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
+import { tags as t } from '@lezer/highlight';
+
+// highlight color
+export const highlights = (isDarkMode: boolean = false) => {
+  return syntaxHighlighting(
+    HighlightStyle.define([
+      { tag: t.keyword, color: isDarkMode ? '#99d066' : '#085bd7' },
+      { tag: t.operator, color: 'red' },
+      { tag: t.annotation, color: isDarkMode ? '#9CDCFE' : 'blue' },
+      { tag: t.number, color: isDarkMode ? '#50fa7b' : '#0c7e5e' },
+      { tag: t.string, color: '#085bd7' },
+      { tag: t.url, color: '#000' },
+      // { tag: t.function, color: "blue" },
+      { tag: t.lineComment, color: '#a2a2a2', fontStyle: 'italic' },
+      { tag: t.comment, color: '#a2a2a2', fontStyle: 'italic' },
+    ])
+  );
+};

+ 1781 - 0
client/src/pages/play/language/extensions/json/openapi.json

@@ -0,0 +1,1781 @@
+[
+  {
+    "name": "databases",
+    "children": [
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "properties",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "database.replica.number",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.resource_groups",
+                "type": "string",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.diskQuota.mb",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.max.collections",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.force.deny.writing",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.force.deny.reading",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "list",
+        "children": []
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "alter",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "properties",
+            "type": "object",
+            "required": true,
+            "children": [
+              {
+                "name": "database.replica.number",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.resource_groups",
+                "type": "string",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.diskQuota.mb",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.max.collections",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.force.deny.writing",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "database.force.deny.reading",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "drop_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "propertyKeys",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "entities",
+    "children": [
+      {
+        "name": "delete",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "filter",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "insert",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "data",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "upsert",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "data",
+            "type": "object | array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "query",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "filter",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "outputFields",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "limit",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "offset",
+            "type": "integer",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "search",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "data",
+            "type": "array",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "annsField",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "filter",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "limit",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "offset",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "groupingField",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "outputFields",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "searchParams",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "metricType",
+                "type": "string",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "params",
+                "type": "object",
+                "required": false,
+                "children": [
+                  {
+                    "name": "radius",
+                    "type": "integer",
+                    "required": false,
+                    "children": []
+                  },
+                  {
+                    "name": "range_filter",
+                    "type": "integer",
+                    "required": false,
+                    "children": []
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "consistencyLevel",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "hybrid_search",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "search",
+            "type": "array",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "rerank",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "strategy",
+                "type": "string",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "params",
+                "type": "object",
+                "required": true,
+                "children": [
+                  {
+                    "name": "k",
+                    "type": "integer",
+                    "required": true,
+                    "children": []
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "name": "limit",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "outputFields",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "consistencyLevel",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "get",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "id",
+            "type": "string | integer | string[] | integer[]",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "outputFields",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": false,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "collections",
+    "children": [
+      {
+        "name": "list",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "children": []
+      },
+      {
+        "name": "flush",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "compact",
+        "children": [
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "has",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "get_stats",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "refresh_load",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "alter_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "properties",
+            "type": "object",
+            "required": true,
+            "children": [
+              {
+                "name": "mmmap.enabled",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "collection.ttl.seconds",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "partitionkey.isolation",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "drop_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "propertyKeys",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "fields/alter_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "fieldName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "fieldParams",
+            "type": "object",
+            "required": true,
+            "children": [
+              {
+                "name": "max_length",
+                "type": "integer",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "max_capacity",
+                "type": "integer",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "rename",
+        "children": [
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "newDbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "newCollectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "load",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "release",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "get_load_state",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "indexes",
+    "children": [
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "indexParams",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "indexName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "alter_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "indexName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "properties",
+            "type": "object",
+            "required": true,
+            "children": [
+              {
+                "name": "mmap.enabled",
+                "type": "boolean",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "drop_properties",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "indexName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "propertyKeys",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "indexName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "list",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "partitions",
+    "children": [
+      {
+        "name": "list",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "load",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "release",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionNames",
+            "type": "array",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "has",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "get_stats",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "roles",
+    "children": [
+      {
+        "name": "list",
+        "children": []
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "grant_privilege",
+        "children": [
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "objectType",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "objectName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "privilege",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "revoke_privilege",
+        "children": [
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "objectType",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "objectName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "privilege",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "jobs",
+    "children": [
+      {
+        "name": "import/create",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "clusterId",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "partitionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "files",
+            "type": "array",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "options",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "timeout",
+                "type": "string",
+                "required": false,
+                "children": []
+              }
+            ]
+          },
+          {
+            "name": "objectUrl",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "accessKey",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "secretKey",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "token",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "import/list",
+        "children": [
+          {
+            "name": "clusterId",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "pageSize",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "currentPage",
+            "type": "integer",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "import/getProgress",
+        "children": [
+          {
+            "name": "jobId",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "clusterId",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "import/describe",
+        "children": [
+          {
+            "name": "clusterId",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "jobId",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "aliases",
+    "children": [
+      {
+        "name": "list",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "aliasName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "alter",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "aliasName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "aliasName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "dbName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "aliasName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "users",
+    "children": [
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "password",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "list",
+        "children": []
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "update_password",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "password",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "newPassword",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "grant_role",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "revoke_role",
+        "children": [
+          {
+            "name": "userName",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "roleName",
+            "type": "string",
+            "required": true,
+            "children": []
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "name": "resource_groups",
+    "children": [
+      {
+        "name": "create",
+        "children": [
+          {
+            "name": "name",
+            "type": "string",
+            "required": true,
+            "children": []
+          },
+          {
+            "name": "config",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "requests",
+                "type": "object",
+                "required": false,
+                "children": [
+                  {
+                    "name": "node_num",
+                    "type": "integer",
+                    "required": false,
+                    "children": []
+                  }
+                ]
+              },
+              {
+                "name": "limits",
+                "type": "object",
+                "required": false,
+                "children": [
+                  {
+                    "name": "node_num",
+                    "type": "integer",
+                    "required": false,
+                    "children": []
+                  }
+                ]
+              },
+              {
+                "name": "transfer_from",
+                "type": "array",
+                "required": false,
+                "children": []
+              },
+              {
+                "name": "transfer_to",
+                "type": "array",
+                "required": false,
+                "children": []
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "alter",
+        "children": [
+          {
+            "name": "resource_groups",
+            "type": "object",
+            "required": false,
+            "children": [
+              {
+                "name": "<resource_group_name>",
+                "type": "object",
+                "required": false,
+                "children": [
+                  {
+                    "name": "requests",
+                    "type": "object",
+                    "required": false,
+                    "children": [
+                      {
+                        "name": "node_num",
+                        "type": "integer",
+                        "required": false,
+                        "children": []
+                      }
+                    ]
+                  },
+                  {
+                    "name": "limits",
+                    "type": "object",
+                    "required": false,
+                    "children": [
+                      {
+                        "name": "node_num",
+                        "type": "integer",
+                        "required": false,
+                        "children": []
+                      }
+                    ]
+                  },
+                  {
+                    "name": "transfer_from",
+                    "type": "array",
+                    "required": false,
+                    "children": []
+                  },
+                  {
+                    "name": "transfer_to",
+                    "type": "array",
+                    "required": false,
+                    "children": []
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "describe",
+        "children": [
+          {
+            "name": "name",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "list",
+        "children": []
+      },
+      {
+        "name": "drop",
+        "children": [
+          {
+            "name": "name",
+            "type": "string",
+            "required": false,
+            "children": []
+          }
+        ]
+      },
+      {
+        "name": "transfer_replica",
+        "children": [
+          {
+            "name": "sourceRgName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "targetRgName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "collectionName",
+            "type": "string",
+            "required": false,
+            "children": []
+          },
+          {
+            "name": "replicaNum",
+            "type": "integer",
+            "required": false,
+            "children": []
+          }
+        ]
+      }
+    ]
+  }
+]

+ 7 - 0
client/src/pages/play/language/extensions/keymap.ts

@@ -0,0 +1,7 @@
+import { keymap } from '@codemirror/view';
+import { indentWithTab } from '@codemirror/commands';
+import { acceptCompletion } from '@codemirror/autocomplete';
+
+export function KeyMap() {
+  return keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]);
+}

+ 42 - 0
client/src/pages/play/language/extensions/linter.ts

@@ -0,0 +1,42 @@
+import { lintGutter, linter, Diagnostic } from '@codemirror/lint';
+import { EditorView } from '@codemirror/view';
+import { syntaxTree } from '@codemirror/language';
+
+export const milvusHttpLinter = [
+  lintGutter(), // Enable the lint gutter to display error indicators
+  linter((view: EditorView) => {
+    const state = view.state;
+    const tree = syntaxTree(state); // Get the syntax tree of the current document
+
+    // Array to store linting diagnostics (errors/warnings)
+    const diagnostics: Diagnostic[] = [];
+
+    // Iterate through the syntax tree
+    tree.iterate({
+      enter(node) {
+        if (node.type.isError) {
+          // Get the start and end positions of the error node
+          const from = node.from;
+          const to = node.to;
+
+          // check if this line is empty
+          const line = state.doc.lineAt(from);
+          if (line.length === 0) {
+            return;
+          }
+
+          // Add the error to the diagnostics array
+          diagnostics.push({
+            from,
+            to,
+            severity: 'error', // Set the severity level (error/warning/info)
+            message: 'Syntax error detected', // Error message to display
+          });
+        }
+      },
+    });
+
+    // Return the list of diagnostics to be displayed in the editor
+    return diagnostics;
+  }),
+];

+ 115 - 0
client/src/pages/play/language/extensions/selectionDecoration.ts

@@ -0,0 +1,115 @@
+import { syntaxTree } from '@codemirror/language';
+import { Range, RangeSetBuilder } from '@codemirror/state';
+import { ViewPlugin, ViewUpdate } from '@codemirror/view';
+import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
+import { Text } from '@codemirror/state';
+
+const requestHighlightLineDecoration = Decoration.line({
+  class: 'milvus-http-request-highlight',
+});
+
+export const selectionDecoration = ViewPlugin.fromClass(
+  class {
+    decorations: DecorationSet;
+
+    constructor(view: EditorView) {
+      this.decorations = this.computeDecorations(view);
+    }
+
+    update(update: ViewUpdate) {
+      if (update.docChanged || update.selectionSet) {
+        this.decorations = this.computeDecorations(update.view);
+      }
+    }
+
+    computeDecorations(view: EditorView) {
+      const state = view.state;
+      const doc = state.doc;
+      const tree = syntaxTree(state);
+      const cursor = state.selection.main.head;
+
+      let nodeAtCursor = tree.resolve(cursor, -1);
+
+      const builder = new RangeSetBuilder<Decoration>();
+
+      let requestNode = null;
+
+      while (nodeAtCursor) {
+        if (nodeAtCursor.name === 'Request') {
+          requestNode = nodeAtCursor;
+          break;
+        }
+        if (!nodeAtCursor.parent) {
+          break;
+        }
+        nodeAtCursor = nodeAtCursor.parent;
+      }
+
+      if (requestNode) {
+        // get from and to positions of the request node
+        const from = requestNode.from;
+
+        // get URL node
+        const urlNode = requestNode.getChild('URL')!;
+        const bodyNode = requestNode.getChild('Body');
+
+        // if no URL node and no Body node, return
+        if (!urlNode && !bodyNode) {
+          return builder.finish();
+        }
+
+        // get to position of the Body node or URL node
+        const to = bodyNode ? bodyNode.to : urlNode.to;
+
+        // highlight the whole request node
+        const lines = this.getLinesInRange(doc, from, to);
+
+        lines.forEach(line => {
+          builder.add(line.from, line.from, requestHighlightLineDecoration);
+        });
+      }
+
+      return builder.finish();
+    }
+
+    getLinesInRange(doc: Text, from: number, to: number) {
+      try {
+        const lines = [];
+
+        let startLine = doc.lineAt(from);
+        let endLine = doc.lineAt(to);
+
+        for (let i = startLine.number; i <= endLine.number; i++) {
+          const line = doc.line(i);
+          lines.push(line);
+        }
+
+        return lines;
+      } catch (error) {
+        return [];
+      }
+    }
+  },
+  {
+    decorations: v => v.decorations,
+  }
+);
+
+const tokenClass = Decoration.mark({ class: 'token-node' });
+export const highlightTokens = EditorView.decorations.compute(
+  ['doc'],
+  state => {
+    const decorations: Range<Decoration>[] = [];
+    const tree = syntaxTree(state);
+
+    tree.iterate({
+      enter(node) {
+        if (node.name === 'Authorization') {
+          decorations.push(tokenClass.range(node.from, node.to));
+        }
+      },
+    });
+
+    return Decoration.set(decorations);
+  }
+);

+ 112 - 0
client/src/pages/play/language/milvus.http.grammar

@@ -0,0 +1,112 @@
+@top MultipleRequests { (Authorization space)? Request+ }
+
+Request { ( HTTPMethod " " URL space Body space*) | HTTPMethod " " URL space* }
+
+Authorization {
+  ("TOKEN" " " UnquotedString) | ("TOKEN=" '"' UnquotedString '"')
+}
+
+HTTPMethod {
+  "GET" | "POST" | "PUT" | "DELETE"
+}
+
+URL {
+  Resource API
+}
+
+Resource {
+  "/v2/vectordb/"
+}
+
+API {
+  Aliases | Collections | Databases | Partitions |  Jobs |  Indexes | ResourceGroups | Roles | Users | Vectors
+}
+
+Aliases {
+  "aliases/" ("alter" | "create" | "describe" | "drop" | "list")
+}
+
+Databases {
+  "databases/" ("alter" | "create" | "describe" | "drop_properties" | "drop" | "list")
+}
+
+Collections {
+  "collections/" ("fields/alter_properties" | "alter_properties" | "compact" | "create" | "describe" | "drop_properties" | "drop" | "flush" | "get_load_state" | "get_stats" | "has" | "list" | "load" | "release" | "rename" | "refresh_load")
+}
+
+Partitions {
+  "partitions/" ("create" | "drop" | "get_stats" | "has" | "list" | "load" | "release")
+}
+
+Jobs {
+  "jobs/" ("import/create" | "import/describe" | "import/list")
+}
+
+Indexes {
+  "indexes/" ("alter_properties" | "create" | "describe" | "drop_properties" | "drop" | "list")
+}
+
+ResourceGroups {
+  "resource_groups/" ("create" | "describe" | "drop" | "list" | "transfer_replica" | "alter")
+}
+
+Roles {
+  "roles/" ("create" | "describe" | "drop" | "grant_privilege" | "list" | "revoke_privilege")
+}
+
+Users {
+  "users/" ("create" | "describe" | "drop" | "grant_role" | "list" | "revoke_role" | "update_password")
+}
+
+Vectors {
+  "entities/" ("delete" | "get" | "hybrid_search" | "insert" | "query" | "search" | "upsert")
+}
+
+Body {
+  "{" (Property ("," Property)*)? "}"
+}
+
+Property {
+  space* Identifier space* ":" space* Value space*
+}
+
+Value {
+  String
+  | Number
+  | Boolean
+  | "null"
+  | Body
+  | Array
+}
+
+Array {
+  "[" (space* Value space* ("," space* Value space*)*)? "]"
+}
+
+
+@tokens {
+  LineComment { "#" ![\n]* }
+
+  String {
+    '"' (![\\\n"] | "\\" _)* '"'?
+  }
+  UnquotedString {
+    $[a-zA-Z0-9_:-]+
+  }
+  Identifier { String }
+  number {
+    (@digit+ ("." @digit*)? | "." @digit+) (("e" | "E") ("+" | "-")? @digit+)? |
+    "0x" (@digit | $[a-fA-F])+ |
+    "0b" $[01]+ |
+    "0o" $[0-7]+
+  }
+
+  Number { number | "-" number }
+  Boolean { "true" | "false" }
+
+  space { @whitespace+ }
+}
+
+@skip {
+   LineComment
+}

Plik diff jest za duży
+ 11 - 0
client/src/pages/play/language/milvus.http.parser.js


+ 29 - 0
client/src/pages/play/language/milvus.http.parser.terms.js

@@ -0,0 +1,29 @@
+// This file was generated by lezer-generator. You probably shouldn't edit it.
+export const
+  LineComment = 1,
+  MultipleRequests = 2,
+  Authorization = 3,
+  UnquotedString = 4,
+  Request = 5,
+  HTTPMethod = 6,
+  URL = 7,
+  Resource = 8,
+  API = 9,
+  Aliases = 10,
+  Collections = 11,
+  Databases = 12,
+  Partitions = 13,
+  Jobs = 14,
+  Indexes = 15,
+  ResourceGroups = 16,
+  Roles = 17,
+  Users = 18,
+  Vectors = 19,
+  Body = 20,
+  Property = 21,
+  Identifier = 22,
+  Value = 23,
+  String = 24,
+  Number = 25,
+  Boolean = 26,
+  Array = 27

+ 46 - 0
client/src/pages/play/language/milvus.http.ts

@@ -0,0 +1,46 @@
+import { styleTags, tags as t } from '@lezer/highlight';
+import { LRLanguage } from '@codemirror/language';
+import { LanguageSupport } from '@codemirror/language';
+
+import { parser } from './milvus.http.parser';
+import {
+  selectionDecoration,
+  highlightTokens,
+} from './extensions/selectionDecoration';
+import { highlights } from './extensions/highlights';
+import { milvusHttpLinter } from './extensions/linter';
+import { codeLensDecoration } from './extensions/codelens';
+
+import { PlaygroundExtensionParams } from '../Types';
+
+const parserWithMetadata = parser.configure({
+  props: [
+    styleTags({
+      API: t.url,
+      HTTPMethod: t.annotation,
+      VERSION: t.annotation,
+      Identifier: t.keyword,
+      Query: t.string,
+      LineComment: t.comment,
+      Number: t.number,
+    }),
+  ],
+});
+
+export const milvusHttp = LRLanguage.define({
+  parser: parserWithMetadata,
+  languageData: {
+    commentTokens: { line: ';' },
+  },
+});
+
+export function MilvusHTTP(params: PlaygroundExtensionParams) {
+  const { isDarkMode, ...restParams } = params;
+  return new LanguageSupport(milvusHttp, [
+    highlights(isDarkMode),
+    selectionDecoration,
+    highlightTokens,
+    milvusHttpLinter,
+    codeLensDecoration(restParams),
+  ]);
+}

+ 222 - 0
client/src/pages/play/style.tsx

@@ -0,0 +1,222 @@
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+
+export const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    margin: '0',
+    position: 'relative',
+    display: 'flex',
+    overflow: 'hidden',
+    borderRadius: 8,
+    height: '100vh',
+    padding: theme.spacing(2),
+  },
+  leftPane: {
+    flex: 1,
+    padding: 0,
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  rightPane: {
+    flex: 1,
+    padding: `0 ${theme.spacing(2)}`,
+    marginLeft: theme.spacing(2),
+    display: 'flex',
+    overflow: 'hidden',
+  },
+  response: {
+    width: '100%',
+    overflow: 'auto',
+    lineHeight: '18px',
+
+    '& pre': {
+      borderRadius: '4px',
+    },
+  },
+  editor: {
+    width: '100%',
+    height: '100%',
+    border: 'none',
+    outline: 'none',
+    resize: 'none',
+    fontSize: '16px',
+    fontFamily: 'monospace',
+    backgroundColor: 'transparent',
+  },
+}));
+
+export const getCMStyle = (theme: Theme) => {
+  const isDark = theme.palette.mode === 'dark';
+  return {
+    '&.cm-editor': {
+      backgroundColor: theme.palette.background.paper,
+      color: theme.palette.text.primary,
+      height: '100%',
+      borderRadius: '4px',
+    },
+    '&.cm-editor .cm-cursor': {
+      borderColor: theme.palette.text.primary,
+    },
+    '&.cm-editor .cm-scroller': {
+      padding: '12px 0',
+      borderRadius: '4px',
+      overflow: 'auto',
+    },
+    '.cm-line': { padding: ' 0 4px 0 2px' },
+    '.cm-content': {
+      fontSize: '13px',
+      fontFamily: 'IBM Plex Mono, monospace',
+      padding: 0,
+    },
+    '.cm-activeLine': { backgroundColor: 'transparent' },
+    '.cm-gutters': {
+      fontSize: '13px',
+      backgroundColor: theme.palette.background.paper,
+      color: theme.palette.text.primary,
+      border: 'none',
+    },
+    '.cm-lineNumbers .cm-gutterElement': {
+      padding: '0 2px 0 20px',
+    },
+    '.cm-activeLineGutter': {
+      backgroundColor: 'transparent',
+      color: theme.palette.text.primary,
+    },
+    '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
+      {
+        backgroundColor: theme.palette.primary.main,
+        color: theme.palette.primary.contrastText,
+      },
+    // auto completion box style
+    '.cm-tooltip.cm-tooltip-autocomplete': {
+      border: 'none',
+      borderRadius: '4px',
+      transform: 'translateX(-20px)', // adjust box position to align with the text
+      backgroundColor:
+        theme.palette.mode === 'dark'
+          ? theme.palette.grey[800]
+          : theme.palette.background.paper,
+    },
+    '.cm-tooltip.cm-tooltip-autocomplete>ul': {
+      fontFamily: 'IBM Plex Mono, monospace',
+      maxHeight: '208px',
+      border: '1px solid transparent',
+      borderRadius: '4px',
+      boxShadow: '0 4px 16px rgb(52 56 59 / 13%)',
+    },
+    '.cm-tooltip.cm-tooltip-autocomplete>ul li': {
+      display: 'flex',
+      gap: '12px',
+      flexDirection: 'row',
+      maxWidth: '400px',
+      minWidth: '300px',
+      whiteSpace: 'normal',
+      paddingTop: '6px',
+      border: 'none',
+      borderColor: '#e2e3e5',
+      paddingBottom: '8px',
+      padding: '6px 12px 8px',
+      transition: 'background 0.2s ease-in-out',
+    },
+    '.cm-tooltip-autocomplete .cm-completionLabel': {
+      flex: 1,
+      display: 'block',
+      fontSize: '13.5px',
+      fontWeight: 500,
+      order: 1,
+    },
+    '.cm-tooltip-autocomplete .cm-completionLabel .cm-completionMatchedText': {
+      textDecoration: 'none',
+      fontWeight: 'bold',
+    },
+    '.cm-tooltip-autocomplete .cm-completionIcon': {
+      display: 'block',
+      fontSize: '13.5px',
+      order: 2,
+    },
+    '.cm-tooltip-autocomplete .cm-completionDetail': {
+      display: 'block',
+      fontSize: '13.5px',
+      order: 2,
+    },
+    '.cm-tooltip-autocomplete>ul>li[aria-selected=true]': {
+      color: theme.palette.text.primary,
+      backgroundColor: isDark
+        ? 'rgba(10, 206, 130, 0.4)'
+        : 'rgba(10, 206, 130, 0.2)',
+    },
+    '.cm-tooltip-autocomplete>ul>li[aria-selected=true] .cm-autocomplete-option-tab-badge':
+      {
+        opacity: 1,
+      },
+    '.cm-tooltip-autocomplete .cm-autocomplete-option-tab-badge': {
+      display: 'inline-block',
+      fontSize: '10px',
+      color: theme.palette.primary.main,
+      border: `1px solid ${theme.palette.primary.main}`,
+      borderRadius: '2px',
+      padding: '0 2px',
+      height: '14px',
+      lineHeight: '14px',
+      opacity: 0,
+      transition: 'opacity 0.2s ease-in-out',
+      order: 3,
+    },
+    '.milvus-http-request-highlight': {
+      backgroundColor: isDark
+        ? 'rgba(255, 255, 255, 0.15)'
+        : 'rgba(10, 206, 130, 0.08)',
+      borderRadius: '3px',
+    },
+    '.milvus-http-request-error': {
+      backgroundColor: 'rgba(255, 0, 0, 0.2)',
+      borderRadius: '3px',
+    },
+    '.playground-toolbar': {
+      display: 'inline',
+      backgroundColor: 'rgba(0, 0, 0, 0.1)',
+      borderRadius: '4px',
+    },
+    '.playground-codelens': {
+      display: 'flex',
+      alignItems: 'center',
+    },
+    '.playground-codelens .codelens-item': {
+      position: 'relative',
+      color: isDark ? '#999999' : '#919191',
+      cursor: 'pointer',
+      fontSize: '11px',
+      lineHeight: '17px',
+      letterSpacing: '0.5px',
+      textDecoration: 'none',
+      transition: 'color 0.2s ease-in-out',
+
+      '&:hover': {
+        color: theme.palette.primary.main,
+      },
+
+      '&:not(:last-child)': {
+        marginRight: '16px',
+
+        '&::after': {
+          position: 'absolute',
+          right: '-8px',
+          top: '3px',
+          display: 'inline-block',
+          content: '""',
+          borderRight: `1px solid ${isDark ? '#666666' : '#e0e0e0'}`,
+          height: '10px',
+        },
+      },
+    },
+    '.cm-line .token-node': {
+      color: isDark ? '#50fa7b' : '#006600',
+    },
+    '.cm-line .cm-widgetBuffer': {
+      height: '17px',
+    },
+    '.cm-gutter .cm-codelens-marker': {
+      paddingTop: '17px',
+    },
+  };
+};

+ 29 - 0
client/src/pages/play/utils/event.ts

@@ -0,0 +1,29 @@
+import { type PlaygroundCustomEventDetail, CustomEventNameEnum } from '../Types';
+
+type EventMap = {
+  [CustomEventNameEnum.PlaygroundResponseDetail]: PlaygroundCustomEventDetail;
+};
+
+export class DocumentEventManager {
+  static dispatch<K extends keyof EventMap>(eventName: K, detail: EventMap[K]) {
+    const event = new CustomEvent(eventName, { detail });
+    document.dispatchEvent(event);
+  }
+
+  static subscribe<K extends keyof EventMap>(
+    eventName: K,
+    callback: (event: CustomEvent<EventMap[K]>) => void
+  ) {
+    document.addEventListener(eventName, callback as EventListener);
+    return () => {
+      document.removeEventListener(eventName, callback as EventListener);
+    };
+  }
+
+  static unsubscribe<K extends keyof EventMap>(
+    eventName: string,
+    callback: (event: CustomEvent<EventMap[K]>) => void
+  ) {
+    document.removeEventListener(eventName, callback as EventListener);
+  }
+}

+ 2 - 0
client/src/pages/play/utils/index.ts

@@ -0,0 +1,2 @@
+export * from './event';
+export * from './request';

+ 46 - 0
client/src/pages/play/utils/request.ts

@@ -0,0 +1,46 @@
+import axios from 'axios';
+
+type PlaygroundRequestOptions = {
+  url: string;
+  method?: string;
+  host?: string;
+  headers?: Record<string, string | undefined>;
+  params?: Record<string, string>;
+  body?: Record<string, any>;
+};
+
+function isLocalhost(url: string): boolean {
+  const regex =
+    /^(http:\/\/|https:\/\/)?(localhost|127\.0\.0\.1)(:\d+)?(\/.*)?$/;
+  return regex.test(url);
+}
+
+export const createPlaygroundRequest =
+  (type: 'frontend' | 'backend') => (options: PlaygroundRequestOptions) => {
+    const {
+      url,
+      method = 'POST',
+      host = '',
+      headers = {},
+      body = {},
+      params = {},
+    } = options;
+    if (isLocalhost(host) || type === 'frontend') {
+      return axios.request({
+        url,
+        method,
+        headers,
+        baseURL: host,
+        data: body,
+        params,
+      });
+    }
+    return axios.post('/api/v1/playground', {
+      host,
+      url,
+      headers,
+      method,
+      body,
+      params,
+    });
+  };

+ 2 - 0
client/src/router/Router.tsx

@@ -8,6 +8,7 @@ import Index from '@/pages/index';
 import Search from '@/pages/search/VectorSearch';
 import System from '@/pages/system/SystemView';
 import SystemHealthy from '@/pages/systemHealthy/SystemHealthyView';
+import Play from '@/pages/play/Play';
 
 const RouterComponent = () => {
   const { isManaged, isDedicated } = useContext(authContext);
@@ -39,6 +40,7 @@ const RouterComponent = () => {
               <Route path="privilege-groups" element={<Users />} />
             </>
           )}
+          <Route path="play" element={<Play />} />
           {!isManaged && <Route path="system" element={<System />} />}
         </Route>
         <Route path="connect" element={<Connect />} />

+ 1 - 0
client/src/router/consts.ts

@@ -14,4 +14,5 @@ export enum ALL_ROUTER_TYPES {
   USER = 'user',
   DATABASES = 'databases',
   DB_ADMIN = 'db-admin',
+  PLAY = 'play',
 }

+ 58 - 2
client/yarn.lock

@@ -377,6 +377,16 @@
     "@codemirror/view" "^6.17.0"
     "@lezer/common" "^1.0.0"
 
+"@codemirror/autocomplete@^6.18.4":
+  version "6.18.4"
+  resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz#4394f55d6771727179f2e28a871ef46bbbeb11b1"
+  integrity sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==
+  dependencies:
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.17.0"
+    "@lezer/common" "^1.0.0"
+
 "@codemirror/commands@^6.0.0", "@codemirror/commands@^6.6.0":
   version "6.6.0"
   resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4"
@@ -408,7 +418,7 @@
     "@codemirror/language" "^6.0.0"
     "@lezer/json" "^1.0.0"
 
-"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.1", "@codemirror/language@^6.6.0":
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.6.0":
   version "6.10.2"
   resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
   integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
@@ -420,6 +430,18 @@
     "@lezer/lr" "^1.0.0"
     style-mod "^4.0.0"
 
+"@codemirror/language@^6.10.1":
+  version "6.11.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.0.tgz#5ae90972601497f4575f30811519d720bf7232c9"
+  integrity sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==
+  dependencies:
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.23.0"
+    "@lezer/common" "^1.1.0"
+    "@lezer/highlight" "^1.0.0"
+    "@lezer/lr" "^1.0.0"
+    style-mod "^4.0.0"
+
 "@codemirror/lint@^6.0.0", "@codemirror/lint@^6.8.0":
   version "6.8.1"
   resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.1.tgz#6427848815baaf68c08e98c7673b804d3d8c0e7f"
@@ -772,6 +794,19 @@
   resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
   integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==
 
+"@lezer/common@^1.2.3":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
+  integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
+
+"@lezer/generator@^1.7.2":
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/@lezer/generator/-/generator-1.7.2.tgz#a491c91eb9f117ea803e748fa97574514156a2a3"
+  integrity sha512-CwgULPOPPmH54tv4gki18bElLCdJ1+FBC+nGVSVD08vFWDsMjS7KEjNTph9JOypDnet90ujN3LzQiW3CyVODNQ==
+  dependencies:
+    "@lezer/common" "^1.1.0"
+    "@lezer/lr" "^1.3.0"
+
 "@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
@@ -779,6 +814,13 @@
   dependencies:
     "@lezer/common" "^1.0.0"
 
+"@lezer/highlight@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
+  integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
+  dependencies:
+    "@lezer/common" "^1.0.0"
+
 "@lezer/javascript@^1.0.0":
   version "1.4.17"
   resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.17.tgz#8456e369f960c328b9e823342d0c72d704238c31"
@@ -1180,6 +1222,13 @@
   dependencies:
     "@babel/types" "^7.20.7"
 
+"@types/codemirror@^5.60.15":
+  version "5.60.15"
+  resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.15.tgz#0f82be6f4126d1e59cf4c4830e56dcd49d3c3e8a"
+  integrity sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==
+  dependencies:
+    "@types/tern" "*"
+
 "@types/d3-array@*":
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
@@ -1390,7 +1439,7 @@
     "@types/d3-transition" "*"
     "@types/d3-zoom" "*"
 
-"@types/estree@1.0.6":
+"@types/estree@*", "@types/estree@1.0.6":
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
   integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
@@ -1514,6 +1563,13 @@
     "@types/prop-types" "*"
     csstype "^3.0.2"
 
+"@types/tern@*":
+  version "0.23.9"
+  resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.9.tgz#6f6093a4a9af3e6bb8dde528e024924d196b367c"
+  integrity sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==
+  dependencies:
+    "@types/estree" "*"
+
 "@types/unist@^2":
   version "2.0.10"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"

+ 2 - 0
server/src/app.ts

@@ -12,6 +12,7 @@ import { router as databasesRouter } from './database';
 import { router as partitionsRouter } from './partitions';
 import { router as cronsRouter } from './crons';
 import { router as userRouter } from './users';
+import { router as playgroundRouter } from './playground';
 import {
   TransformResMiddleware,
   LoggingMiddleware,
@@ -51,6 +52,7 @@ router.use('/collections', collectionsRouter);
 router.use('/partitions', partitionsRouter);
 router.use('/crons', cronsRouter);
 router.use('/users', userRouter);
+router.use('/playground', playgroundRouter);
 router.get('/healthy', (req, res, next) => {
   res.json({ status: 200 });
   next();

+ 27 - 0
server/src/playground/dto.ts

@@ -0,0 +1,27 @@
+import { IsNotEmpty, IsString, IsObject, IsOptional } from 'class-validator';
+
+export class PlaygroundRequestDto {
+  @IsNotEmpty({ message: 'method is required' })
+  @IsString()
+  readonly method: string;
+
+  @IsNotEmpty({ message: 'url is required' })
+  @IsString()
+  readonly url: string;
+
+  @IsOptional()
+  @IsString()
+  readonly host?: string;
+
+  @IsOptional()
+  @IsObject()
+  readonly headers?: Record<string, string>;
+
+  @IsOptional()
+  @IsObject()
+  readonly params?: Record<string, string>;
+
+  @IsOptional()
+  @IsObject()
+  readonly body?: Record<string, any>;
+}

+ 8 - 0
server/src/playground/index.ts

@@ -0,0 +1,8 @@
+import { PlaygroundController } from './playground.controller';
+
+const playgroundManager = new PlaygroundController();
+
+const router = playgroundManager.generateRoutes();
+const PlaygroundService = playgroundManager.playgroundServiceGetter;
+
+export { router, PlaygroundService };

+ 41 - 0
server/src/playground/playground.controller.ts

@@ -0,0 +1,41 @@
+import { NextFunction, Request, Response, Router } from 'express';
+import { dtoValidationMiddleware } from '../middleware/validation';
+import { PlaygroundService } from './playground.service';
+import { PlaygroundRequestDto } from './dto';
+
+export class PlaygroundController {
+  private playgroundService: PlaygroundService;
+  private router: Router;
+
+  constructor() {
+    this.playgroundService = new PlaygroundService();
+    this.router = Router();
+  }
+
+  get playgroundServiceGetter() {
+    return this.playgroundService;
+  }
+
+  generateRoutes() {
+    this.router.post(
+      '/',
+      dtoValidationMiddleware(PlaygroundRequestDto),
+      this.handleRequest.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async handleRequest(
+    req: Request<{}, {}, PlaygroundRequestDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    try {
+      const result = await this.playgroundService.makeRequest(req.body);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 28 - 0
server/src/playground/playground.service.ts

@@ -0,0 +1,28 @@
+import axios, { AxiosRequestConfig } from 'axios';
+
+export class PlaygroundService {
+  async makeRequest(data: {
+    method: string;
+    url: string;
+    host?: string;
+    headers?: Record<string, string>;
+    params?: Record<string, string>;
+    body?: Record<string, any>;
+  }) {
+    const config: AxiosRequestConfig = {
+      method: data.method as any,
+      url: data.url,
+      baseURL: data.host,
+      headers: data.headers,
+      params: data.params,
+      data: data.body,
+    };
+
+    try {
+      const response = await axios(config);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików