Skip to content

Commit

Permalink
feat: enable support for copy command as curl (#109)
Browse files Browse the repository at this point in the history
feat: enable support for copy command as curl
user can copy the es query as a curl command to action it in the
terminal

Refs: #104

---------

Signed-off-by: seven <[email protected]>
  • Loading branch information
Blankll authored Oct 6, 2024
1 parent 6fe8053 commit 38585b5
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 124 deletions.
168 changes: 86 additions & 82 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "dockit",
"private": true,
"type": "module",
"version": "0.4.5",
"version": "0.4.6",
"description": "DocKit is a desktop client designed for NoSQL database, support Elasticsearch and OpenSearch across Mac, windows and Linux",
"author": "geekfun <[email protected]>",
"homepage": "ttps://dockit.geekfun.club",
Expand Down
1 change: 1 addition & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './pureObject';
export * from './base64';
export * from './crypto';
export * from './valueConversion';
export * from './requestUtil';
81 changes: 73 additions & 8 deletions src/common/monaco/tokenlizer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Decoration, executeActions, monaco, SearchAction } from './';
import { Decoration, Editor, executeActions, monaco, Range, SearchAction } from './';
import JSON5 from 'json5';
import { CustomError } from '../customError.ts';

Expand Down Expand Up @@ -46,6 +46,23 @@ export const buildSearchToken = (lines: Array<{ lineNumber: number; lineContent:

return searchTokens;
};
export const getPositionAction = (position: Range) => {
return searchTokens.find(({ position: { startLineNumber, endLineNumber } }) => {
return position.startLineNumber >= startLineNumber && position.endLineNumber <= endLineNumber;
});
};
export const getPointerAction = (editor: Editor, tokens: Array<SearchAction>) => {
const { lineNumber } = editor?.getPosition() || {};
if (lineNumber === undefined || lineNumber === null) {
return;
}

return tokens.find(
({ position: { startLineNumber, endLineNumber } }) =>
lineNumber >= startLineNumber && lineNumber <= endLineNumber,
);
};

export const executionGutterClass = 'execute-button-decoration';
export const getActionMarksDecorations = (searchTokens: SearchAction[]): Array<Decoration> => {
return searchTokens
Expand All @@ -57,8 +74,22 @@ export const getActionMarksDecorations = (searchTokens: SearchAction[]): Array<D
.filter(Boolean)
.sort((a, b) => (a as Decoration).id - (b as Decoration).id) as Array<Decoration>;
};
export const buildCodeLens = (searchTokens: SearchAction[], autoIndentCmdId: string) =>
searchTokens
export const buildCodeLens = (
searchTokens: SearchAction[],
autoIndentCmdId: string,
copyAsCurlCmdId: string,
) => {
const copyCurl = searchTokens.map(({ position }, index) => ({
range: { ...position, endLineNumber: position.startLineNumber },
id: `CopyAsCurl-${index}`,
command: {
id: copyAsCurlCmdId!,
title: 'Copy as CURL',
arguments: [position],
},
}));

const autoIndent = searchTokens
.filter(({ qdsl }) => qdsl)
.map(({ position }, index) => ({
range: { ...position, endLineNumber: position.startLineNumber },
Expand All @@ -70,6 +101,9 @@ export const buildCodeLens = (searchTokens: SearchAction[], autoIndentCmdId: str
},
}));

return [...autoIndent, ...copyCurl];
};

export const formatQDSL = (
searchTokens: SearchAction[],
model: monaco.editor.ITextModel,
Expand All @@ -93,19 +127,50 @@ export const formatQDSL = (
return lines.map(line => JSON5.stringify(line)).join('\n');
};

export const transformQDSL = (action: SearchAction) => {
export const transformQDSL = ({ path, qdsl }: Pick<SearchAction, 'path' | 'qdsl'>) => {
try {
const bulkAction = action.path.includes('_bulk');
const bulkAction = path.includes('_bulk');
if (bulkAction) {
const dsql = action.qdsl
const bulkQdsl = qdsl
.split('\n')
.map(line => JSON.stringify(JSON5.parse(line)))
.join('\n');
return `${dsql}\n`;
return `${bulkQdsl}\n`;
}

return action.qdsl ? JSON.stringify(JSON5.parse(action.qdsl), null, 2) : undefined;
return qdsl ? JSON.stringify(JSON5.parse(qdsl), null, 2) : undefined;
} catch (err) {
throw new CustomError(400, (err as Error).message);
}
};

export const transformToCurl = ({
method,
headers,
qdsl,
url,
ssl,
}: {
url: string;
method: string;
headers: { [key: string]: string };
qdsl: string;
ssl: boolean | undefined;
}) => {
let curlCmd = `curl -X ${method} '${url}'`;

if (url.startsWith('https') && ssl === false) {
curlCmd += ' --insecure';
}

if (headers) {
curlCmd += Object.entries(headers)
.map(([key, value]) => ` -H '${key}: ${value}'`)
.join('');
}
if (qdsl) {
curlCmd += ` -d '${transformQDSL({ path: url, qdsl })}'`;
}

return curlCmd;
};
26 changes: 26 additions & 0 deletions src/common/requestUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { base64Encode } from './base64.ts';

export const buildAuthHeader = (username: string | undefined, password: string | undefined) => {
const authorization =
username || password ? `Basic ${base64Encode(`${username}:${password}`)}` : undefined;
return authorization ? { authorization } : undefined;
};

export const buildURL = (
host: string,
port: number,
index: string | undefined,
path: string | undefined,
queryParameters?: string,
) => {
const trimmedPath = path?.startsWith('/') ? path.slice(1) : path;
const pathWithIndex =
index &&
!['_nodes', '_cluster', '_cat', '_bulk', '_aliases', '_analyze'].includes(
trimmedPath?.split('/')[0] ?? '',
)
? `${index}/${trimmedPath}`
: `${trimmedPath}`;

return `${host}:${port}/${pathWithIndex}${queryParameters ? `?${queryParameters}` : ''}`;
};
17 changes: 3 additions & 14 deletions src/datasources/fetchApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { base64Encode, CustomError, debug } from '../common';
import { buildAuthHeader, buildURL, CustomError, debug } from '../common';
import { lang } from '../lang';
import { invoke } from '@tauri-apps/api/tauri';

Expand All @@ -21,14 +21,6 @@ const handleFetch = (result: { data: unknown; status: number; details: string |
throw new CustomError(result.status, result.details || '');
};

const buildURL = (host: string, port: number, path?: string, queryParameters?: string) => {
let url = `${host}:${port}`;
url += path ?? '';
url += queryParameters ? `?${queryParameters}` : '';

return url;
};

const fetchWrapper = async ({
method,
path,
Expand All @@ -51,13 +43,10 @@ const fetchWrapper = async ({
ssl: boolean;
}) => {
try {
const authorization =
username || password ? `Basic ${base64Encode(`${username}:${password}`)}` : undefined;

const url = buildURL(host, port, path, queryParameters);
const url = buildURL(host, port, undefined, path, queryParameters);
const { data, status, details } = await fetchRequest(url, {
method,
headers: { authorization },
headers: { ...buildAuthHeader(username, password) },
payload,
agent: { ssl },
});
Expand Down
2 changes: 2 additions & 0 deletions src/lang/enUS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export const enUS = {
editor: {
establishedRequired: 'Select a DB instance before execute actions',
invalidJson: 'Invalid JSON format',
copySuccess: 'Copied to clipboard',
copyFailure: 'Failed to copy to clipboard',
},
history: {
empty: 'No history yet',
Expand Down
2 changes: 2 additions & 0 deletions src/lang/zhCN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export const zhCN = {
editor: {
establishedRequired: '请选择执行操作的数据库实例',
invalidJson: '无效的 JSON 格式',
copySuccess: '已复制到粘贴板',
copyFailure: '复制失败',
},
history: {
empty: '无历史记录',
Expand Down
21 changes: 19 additions & 2 deletions src/store/connectionStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia';
import { pureObject } from '../common';
import { buildAuthHeader, buildURL, pureObject } from '../common';
import { loadHttpClient, storeApi } from '../datasources';
import { SearchAction, transformToCurl } from '../common/monaco';

export type Connection = {
id?: number;
Expand Down Expand Up @@ -85,7 +86,6 @@ export const useConnectionStore = defineStore('connectionStore', {
async establishConnection(connection: Connection) {
await this.testConnection(connection);
const client = loadHttpClient(connection);

const data = (await client.get('/_cat/indices', 'format=json')) as Array<{
[key: string]: string;
}>;
Expand Down Expand Up @@ -169,5 +169,22 @@ export const useConnectionStore = defineStore('connectionStore', {
};
return dispatch[method]();
},
queryToCurl({ method, path, index, qdsl, queryParams }: SearchAction) {
const { username, password, host, port, sslCertVerification } = this.established ?? {
host: 'http://localhost',
port: 9200,
username: undefined,
password: undefined,
};
const params = queryParams ? `${queryParams}&format=json` : 'format=json';
const url = buildURL(host, port, index, path, params);

const headers = {
'Content-Type': 'application/json',
...buildAuthHeader(username, password),
};

return transformToCurl({ method, headers, url, ssl: sslCertVerification, qdsl });
},
},
});
47 changes: 30 additions & 17 deletions src/views/editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import {
formatQDSL,
getActionApiDoc,
getActionMarksDecorations,
getPointerAction,
getPositionAction,
monaco,
Range,
SearchAction,
transformQDSL,
} from '../../common/monaco';
Expand All @@ -44,7 +47,7 @@ const { readSourceFromFile, saveSourceToFile } = sourceFileStore;
const { defaultFile } = storeToRefs(sourceFileStore);
const connectionStore = useConnectionStore();
const { searchQDSL } = connectionStore;
const { searchQDSL, queryToCurl } = connectionStore;
const { established } = storeToRefs(connectionStore);
const { getEditorTheme } = appStore;
const { themeType } = storeToRefs(appStore);
Expand All @@ -54,6 +57,7 @@ const { insertBoard } = storeToRefs(chatStore);
// https://github.com/tjx666/adobe-devtools/commit/8055d8415ed3ec5996880b3a4ee2db2413a71c61
let queryEditor: Editor | null = null;
let autoIndentCmdId: string | null = null;
let copyCurlCmdId: string | null = null;
// DOM
const queryEditorRef = ref();
const displayEditorRef = ref();
Expand Down Expand Up @@ -87,7 +91,7 @@ const codeLensProvider = monaco.languages.registerCodeLensProvider('search', {
refreshActionMarks(queryEditor!, searchTokens);
return {
lenses: buildCodeLens(searchTokens, autoIndentCmdId!),
lenses: buildCodeLens(searchTokens, autoIndentCmdId!, copyCurlCmdId!),
dispose: () => {},
};
},
Expand Down Expand Up @@ -157,16 +161,17 @@ const executeQueryAction = async (position: { column: number; lineNumber: number
}
};
const autoIndentAction = (editor: monaco.editor.IStandaloneCodeEditor) => {
const autoIndentAction = (editor: monaco.editor.IStandaloneCodeEditor, position: Range) => {
const model = editor?.getModel();
const { position } = getPointerAction(editor!, searchTokens) || {};
if (!position || !model) {
const action = getPositionAction(position);
if (!action || !model) {
return;
}
const { startLineNumber, endLineNumber } = position;
const { startLineNumber, endLineNumber } = action.position;
try {
const formatted = formatQDSL(searchTokens, model, position);
const formatted = formatQDSL(searchTokens, model, action.position);
model.pushEditOperations(
[],
[
Expand All @@ -192,16 +197,20 @@ const autoIndentAction = (editor: monaco.editor.IStandaloneCodeEditor) => {
}
};
const getPointerAction = (editor: Editor, tokens: Array<SearchAction>) => {
const { lineNumber } = editor?.getPosition() || {};
if (lineNumber === undefined || lineNumber === null) {
const copyCurlAction = (position: Range) => {
const action = getPositionAction(position);
if (!action) {
return;
}
return tokens.find(
({ position: { startLineNumber, endLineNumber } }) =>
lineNumber >= startLineNumber && lineNumber <= endLineNumber,
);
try {
navigator.clipboard.writeText(queryToCurl(action));
message.success(lang.t('editor.copySuccess'));
} catch (err) {
message.error(`${lang.t('editor.copyFailed')}: ${JSON.stringify(err)}`, {
closable: true,
keepAliveOnHover: true,
});
}
};
const setupQueryEditor = (code: string) => {
Expand All @@ -216,7 +225,8 @@ const setupQueryEditor = (code: string) => {
return;
}
autoIndentCmdId = queryEditor.addCommand(0, () => autoIndentAction(queryEditor!));
autoIndentCmdId = queryEditor.addCommand(0, (...args) => autoIndentAction(queryEditor!, args[1]));
copyCurlCmdId = queryEditor.addCommand(0, (...args) => copyCurlAction(args[1]));
queryEditor.onMouseDown(({ event, target }) => {
if (
Expand All @@ -231,7 +241,10 @@ const setupQueryEditor = (code: string) => {
// Auto indent current request
queryEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyI, () => {
autoIndentAction(queryEditor!);
const { position } = getPointerAction(queryEditor!, searchTokens) || {};
if (position) {
autoIndentAction(queryEditor!, position);
}
});
// Toggle Autocomplete
Expand Down

0 comments on commit 38585b5

Please sign in to comment.