├── media ├── kv │ ├── .gitignore │ ├── vscode.css │ └── main.css └── deno-sidebar.svg ├── logo.png ├── .gitignore ├── scripts ├── .vscode │ └── settings.json └── kv │ └── server.ts ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .vscodeignore ├── kv ├── src │ ├── utils.test.ts │ ├── api.ts │ ├── nav.tsx │ ├── utils.ts │ ├── main.tsx │ ├── icons.tsx │ ├── list.tsx │ └── single.tsx └── tsconfig.json ├── src ├── extension.ts └── kv │ ├── extension.ts │ └── webview.ts ├── .eslintrc.json ├── tsconfig.json ├── README.md ├── LICENSE ├── vsc-extension-quickstart.md └── package.json /media/kv/.gitignore: -------------------------------------------------------------------------------- 1 | main.js -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashrock/kivi/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /scripts/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true, 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | src/**/*.ts 11 | node_modules/**/*.ts 12 | -------------------------------------------------------------------------------- /kv/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | const ut = require("node:test"); 2 | const assert = require("node:assert"); 3 | import { queryToKvPrefix } from "./utils"; 4 | 5 | ut("queryToKvPrefix", (t: any) => { 6 | const result = queryToKvPrefix("user,123"); 7 | assert.deepStrictEqual(result, ["user", 123]); 8 | }); 9 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as kvExtension from "./kv/extension"; 3 | 4 | export function activate(context: vscode.ExtensionContext) { 5 | kvExtension.activate(context); 6 | } 7 | 8 | // This method is called when your extension is deactivated 9 | export function deactivate() { 10 | kvExtension.deactivate(); 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "editor.formatOnSave": true 12 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "ES2020", 6 | "outDir": "out", 7 | "lib": ["ES2020"], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "jsx": "react", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": ["scripts", "kv"] 18 | } 19 | -------------------------------------------------------------------------------- /kv/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "jsx": "react" /* Specify what JSX code is generated. */, 5 | "module": "commonjs" /* Specify what module code is generated. */, 6 | "rootDir": "src" /* Specify the root folder within your source files. */, 7 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 8 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 9 | "strict": true /* Enable all strict type-checking options. */, 10 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Default_Build", 8 | "type": "npm", 9 | "script": "watch", 10 | "problemMatcher": "$tsc-watch", 11 | "isBackground": true, 12 | "presentation": { 13 | "reveal": "never" 14 | } 15 | }, 16 | { 17 | "label": "KV_Build", 18 | "type": "npm", 19 | "script": "kv:build", 20 | "isBackground": true, 21 | "presentation": { 22 | "reveal": "never" 23 | } 24 | }, 25 | { 26 | "label": "Build", 27 | "dependsOn": ["KV_Build", "Default_Build"], 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kivi 2 | 3 | Deno KV Explorer 4 | 5 | [Install from VSCode marketplace](https://marketplace.visualstudio.com/items?itemName=hashrock.kivi) 6 | 7 | スクリーンショット 2023-11-29 21 51 39 8 | 9 | スクリーンショット 2023-11-29 21 57 37 10 | 11 | 12 | ## Extension Settings 13 | 14 | - kivi.listFetchSize (default: 100) 15 | - The number of keys to fetch at a time 16 | - kivi.previewValue (default: true) 17 | - Whether to preview values in the explorer 18 | 19 | ## Known Issues 20 | 21 | - Objects like `Date` or `Uint8Array` will not preserved in the edit feature for now. 22 | 23 | ## See also 24 | 25 | - [kview](https://github.com/kitsonk/kview) - Another Deno KV Explorer 26 | - [awesome-deno-kv](https://github.com/hashrock/awesome-deno-kv) 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 hashrock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /media/deno-sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/kv/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | import * as vscode from "vscode"; 4 | import { KvViewProvider } from "./webview"; 5 | import * as path from 'path'; 6 | import { ChildProcess, spawn } from "child_process"; 7 | 8 | let process: ChildProcess | null = null; 9 | let outputChannel: vscode.OutputChannel | null = null; 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | // create output channel 13 | outputChannel = vscode.window.createOutputChannel("kvViewer"); 14 | context.subscriptions.push(outputChannel); 15 | outputChannel.appendLine("kvViewer activate"); 16 | 17 | const workspaceRoute = vscode.workspace.workspaceFolders?.[0].uri.fsPath; 18 | 19 | if (!workspaceRoute) { 20 | vscode.window.showErrorMessage("Please open a workspace"); 21 | return; 22 | } 23 | 24 | const serverSrcPath = path.join( 25 | context.extensionUri.fsPath, 26 | "scripts", 27 | "kv", 28 | "server.ts" 29 | ); 30 | 31 | process = spawn( 32 | "deno", 33 | ["run", "-A", "--unstable", serverSrcPath], 34 | { 35 | cwd: workspaceRoute, 36 | }, 37 | ); 38 | 39 | process?.stdout?.on("data", (data) => { 40 | // Example: 41 | // Listening on port 57168 42 | const text = data.toString(); 43 | const match = text.match(/Listening on port (\d+)/); 44 | if (match) { 45 | const port = match[1]; 46 | console.log("Server listening on port", port); 47 | 48 | const webviewProvider = new KvViewProvider(context.extensionUri, port); 49 | context.subscriptions.push( 50 | vscode.window.registerWebviewViewProvider( 51 | "hashrock.deno.kvView", 52 | webviewProvider, 53 | ), 54 | ); 55 | } 56 | console.log(`Server stdout: ${data}`); 57 | }); 58 | 59 | process?.stderr?.on("data", (data) => { 60 | console.error(`Server stderr: ${data}`); 61 | }); 62 | 63 | process?.on("close", (code) => { 64 | console.log(`child process exited with code ${code}`); 65 | }); 66 | } 67 | 68 | export function deactivate() { 69 | console.log("kvViewer deactivate"); 70 | 71 | if (process) { 72 | process.kill(); 73 | process = null; 74 | } 75 | if (outputChannel) { 76 | outputChannel.appendLine("kvViewer deactivate"); 77 | outputChannel.dispose(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /media/kv/vscode.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. */ 2 | 3 | :root { 4 | --container-paddding: 20px; 5 | --input-padding-vertical: 6px; 6 | --input-padding-horizontal: 4px; 7 | --input-margin-vertical: 4px; 8 | --input-margin-horizontal: 0; 9 | } 10 | 11 | body { 12 | padding: 0 var(--container-paddding); 13 | color: var(--vscode-foreground); 14 | font-size: var(--vscode-font-size); 15 | font-weight: var(--vscode-font-weight); 16 | font-family: var(--vscode-font-family); 17 | background-color: var(--vscode-editor-background); 18 | } 19 | 20 | ol, 21 | ul { 22 | padding-left: var(--container-paddding); 23 | } 24 | 25 | body > *, 26 | form > * { 27 | margin-block-start: var(--input-margin-vertical); 28 | margin-block-end: var(--input-margin-vertical); 29 | } 30 | 31 | *:focus { 32 | outline-color: var(--vscode-focusBorder) !important; 33 | } 34 | 35 | a { 36 | color: var(--vscode-textLink-foreground); 37 | } 38 | 39 | a:hover, 40 | a:active { 41 | color: var(--vscode-textLink-activeForeground); 42 | } 43 | 44 | code { 45 | font-size: var(--vscode-editor-font-size); 46 | font-family: var(--vscode-editor-font-family); 47 | } 48 | 49 | button { 50 | border: none; 51 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 52 | width: 100%; 53 | text-align: center; 54 | outline: 1px solid transparent; 55 | outline-offset: 2px !important; 56 | color: var(--vscode-button-foreground); 57 | background: var(--vscode-button-background); 58 | } 59 | 60 | button:hover { 61 | cursor: pointer; 62 | background: var(--vscode-button-hoverBackground); 63 | } 64 | 65 | button:focus { 66 | outline-color: var(--vscode-focusBorder); 67 | } 68 | 69 | button.secondary { 70 | color: var(--vscode-button-secondaryForeground); 71 | background: var(--vscode-button-secondaryBackground); 72 | } 73 | 74 | button.secondary:hover { 75 | background: var(--vscode-button-secondaryHoverBackground); 76 | } 77 | 78 | input:not([type='checkbox']), 79 | textarea { 80 | display: block; 81 | width: 100%; 82 | border: none; 83 | font-family: var(--vscode-font-family); 84 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 85 | color: var(--vscode-input-foreground); 86 | outline-color: var(--vscode-input-border); 87 | background-color: var(--vscode-input-background); 88 | } 89 | 90 | input::placeholder, 91 | textarea::placeholder { 92 | color: var(--vscode-input-placeholderForeground); 93 | } 94 | -------------------------------------------------------------------------------- /kv/src/api.ts: -------------------------------------------------------------------------------- 1 | export const vscode = acquireVsCodeApi(); 2 | 3 | import superjson from "superjson"; 4 | export type KvKeyPart = Uint8Array | string | number | bigint | boolean; 5 | export type KvKey = KvKeyPart[]; 6 | export type KvValue = unknown; 7 | 8 | export interface KvPair { 9 | key: KvKey; 10 | value: string; 11 | versionstamp: string; 12 | } 13 | 14 | export interface Config { 15 | previewValue: boolean; 16 | listFetchSize: number; 17 | } 18 | 19 | let id = 0; 20 | 21 | export async function postMessageParent(type: string, body: object) { 22 | id++; 23 | 24 | return new Promise((resolve, reject) => { 25 | const timeoutSec = 60; // TODO databaseChange will timeout but user may still pending to select database 26 | const onTimedout = setTimeout(() => { 27 | reject(`Timeout: ${timeoutSec} seconds.`); 28 | }, timeoutSec * 1000); 29 | 30 | const handler = (event: MessageEvent) => { 31 | // eslint-disable-next-line curly 32 | if (event.data.id !== id) return; 33 | 34 | window.removeEventListener("message", handler); 35 | clearTimeout(onTimedout); 36 | resolve(event.data.result); 37 | }; 38 | window.addEventListener("message", handler); 39 | vscode.postMessage({ 40 | type, 41 | id, 42 | ...body, 43 | source: "webview", 44 | }); 45 | }); 46 | } 47 | 48 | export function kvSet(key: KvKey, value: KvValue) { 49 | return postMessageParent("set", { key, value }); 50 | } 51 | 52 | export async function kvGet(key: KvKey): Promise { 53 | const resultStr = await (postMessageParent("get", { key })) as string; 54 | return superjson.parse(resultStr); 55 | } 56 | 57 | export function kvDelete(key: KvKey) { 58 | return postMessageParent("delete", { key }); 59 | } 60 | 61 | export async function kvList( 62 | key: KvKey, 63 | limit: number, 64 | cursor: string, 65 | ): Promise<{ result: KvPair[]; cursor: string }> { 66 | const resultStr = 67 | await (postMessageParent("list", { key, limit, cursor })) as string; 68 | return superjson.parse(resultStr); 69 | } 70 | 71 | export function kvRequestChangeDatabase() { 72 | // null is cancel 73 | return postMessageParent("changeDatabase", {}) as Promise; 74 | } 75 | 76 | export function showMessage(message: string) { 77 | return postMessageParent("message", { message }); 78 | } 79 | 80 | export async function getConfig() { 81 | const resultStr = 82 | await (postMessageParent("getConfig", {}) as Promise); 83 | return JSON.parse(resultStr); 84 | } 85 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kivi", 3 | "displayName": "kivi", 4 | "description": "Deno KV viewer and editor", 5 | "repository": "https://github.com/hashrock/kivi", 6 | "license": "MIT", 7 | "version": "1.0.6", 8 | "publisher": "hashrock", 9 | "icon": "logo.png", 10 | "engines": { 11 | "vscode": "^1.81.0" 12 | }, 13 | "categories": [ 14 | "Other", 15 | "Snippets" 16 | ], 17 | "keywords": [ 18 | "deno", 19 | "kv" 20 | ], 21 | "activationEvents": [ 22 | "workspaceContains:deno.json" 23 | ], 24 | "contributes": { 25 | "views": { 26 | "kivi": [ 27 | { 28 | "type": "webview", 29 | "id": "hashrock.deno.kvView", 30 | "name": "KV Explorer" 31 | } 32 | ] 33 | }, 34 | "viewsContainers": { 35 | "activitybar": [ 36 | { 37 | "id": "kivi", 38 | "title": "Kivi", 39 | "icon": "media/deno-sidebar.svg" 40 | } 41 | ] 42 | }, 43 | "commands": [], 44 | "menus": { 45 | "explorer/context": [] 46 | }, 47 | "configuration": { 48 | "title": "Kivi", 49 | "properties": { 50 | "kivi.listFetchSize": { 51 | "type": "number", 52 | "default": 100, 53 | "description": "The number of keys to fetch at a time." 54 | }, 55 | "kivi.previewValue": { 56 | "type": "boolean", 57 | "default": true, 58 | "description": "Whether to preview values in the explorer." 59 | } 60 | } 61 | } 62 | }, 63 | "main": "./out/extension.js", 64 | "scripts": { 65 | "vscode:prepublish": "npm run compile", 66 | "compile": "tsc -p ./", 67 | "watch": "tsc -watch -p ./", 68 | "pretest": "npm run compile && npm run lint", 69 | "lint": "eslint src --ext ts", 70 | "test": "node -r esbuild-runner/register kv/src/utils.test.ts", 71 | "changelog": "npx standard-version", 72 | "kv:build": "esbuild kv/src/main.tsx --bundle --outfile=media/kv/main.js --minify --sourcemap=inline" 73 | }, 74 | "devDependencies": { 75 | "@types/mocha": "^10.0.1", 76 | "@types/node": "16.x", 77 | "@types/node-fetch": "^2.6.6", 78 | "@types/react": "^18.2.25", 79 | "@types/react-dom": "^18.2.10", 80 | "@types/react-transition-group": "^4.4.7", 81 | "@types/vscode": "^1.81.0", 82 | "@types/vscode-webview": "^1.57.2", 83 | "@typescript-eslint/eslint-plugin": "^6.4.1", 84 | "@typescript-eslint/parser": "^6.4.1", 85 | "@vscode/test-electron": "^2.3.4", 86 | "classnames": "^2.3.2", 87 | "esbuild": "0.19.4", 88 | "eslint": "^8.47.0", 89 | "glob": "^10.3.3", 90 | "mocha": "^10.2.0", 91 | "react": "^18.2.0", 92 | "react-dom": "^18.2.0", 93 | "react-transition-group": "^4.4.5", 94 | "typescript": "^5.1.6" 95 | }, 96 | "dependencies": { 97 | "esbuild-runner": "^2.2.2", 98 | "node-fetch": "^2.7.0", 99 | "superjson": "1.13.3" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /kv/src/nav.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | 4 | import React, { useCallback, useEffect } from "react"; 5 | import { PageType } from "./main"; 6 | import { IconChevronLeft, IconDots, IconPlus } from "./icons"; 7 | import classnames from "classnames"; 8 | export function BackHome(props: React.ButtonHTMLAttributes) { 9 | return ( 10 | 16 | ); 17 | } 18 | 19 | export function NewItem(props: React.ButtonHTMLAttributes) { 20 | return ( 21 | 27 | ); 28 | } 29 | export interface MenuItemProps { 30 | title: string; 31 | onClick: () => void; 32 | } 33 | function Menu(props: React.ButtonHTMLAttributes) { 34 | return ( 35 | 41 | ); 42 | } 43 | 44 | interface NavProps { 45 | onChangePage: (page: PageType) => void; 46 | children?: React.ReactNode[] | React.ReactNode; 47 | menuItems: MenuItemProps[]; 48 | } 49 | 50 | export function Nav(props: NavProps) { 51 | const [isMenuOpen, setIsMenuOpen] = React.useState(false); 52 | 53 | const clickOutside = useCallback((ev: MouseEvent) => { 54 | if (ev.target instanceof HTMLElement && ev.target.closest(".nav__menu")) { 55 | return; 56 | } 57 | setIsMenuOpen(false); 58 | }, []); 59 | 60 | const onKeydown = useCallback((ev: KeyboardEvent) => { 61 | if (ev.key === "Escape") { 62 | setIsMenuOpen(false); 63 | } 64 | }, []); 65 | 66 | useEffect(() => { 67 | document.addEventListener("click", clickOutside); 68 | document.addEventListener("keydown", onKeydown); 69 | 70 | return () => { 71 | document.removeEventListener("click", clickOutside); 72 | document.removeEventListener("keydown", onKeydown); 73 | }; 74 | }, []); 75 | 76 | return ( 77 |
78 | {props.children} 79 | 80 | { 82 | ev.stopPropagation(); 83 | setIsMenuOpen(!isMenuOpen); 84 | }} 85 | /> 86 | 87 |
90 | {props.menuItems.map((item) => ( 91 |
{ 94 | item.onClick(); 95 | setIsMenuOpen(false); 96 | }} 97 | > 98 | {item.title} 99 |
100 | ))} 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /kv/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { KvKey, KvKeyPart } from "./api"; 2 | 3 | function evalSinglePart(part: string): KvKeyPart { 4 | // Manual input 5 | const RE_NUM = /^[0-9][0-9]*(\.[0-9]*)?$/; 6 | const RE_BIGINT = /^[0-9]+n$/; 7 | const RE_U8 = /^0x([a-fA-F0-9][a-fA-F0-9])+$/; 8 | const RE_QUOTED_STRING = /^".*"$/; 9 | 10 | if (RE_NUM.test(part)) { 11 | return Number(part); 12 | } else if (RE_BIGINT.test(part)) { 13 | return BigInt(part.slice(0, -1)); 14 | } else if (RE_U8.test(part)) { 15 | return Uint8Array.from( 16 | part 17 | .slice(2) 18 | .match(/.{1,2}/g)! 19 | .map((byte) => parseInt(byte, 16)), 20 | ); 21 | } else if (RE_QUOTED_STRING.test(part)) { 22 | return part.slice(1, -1); 23 | } else if (part === "true") { 24 | return true; 25 | } else if (part === "false") { 26 | return false; 27 | } else { 28 | return part; 29 | } 30 | } 31 | 32 | export function queryToKvPrefix(input: string): KvKey { 33 | if (input === "") { 34 | return [] as KvKeyPart[]; 35 | } 36 | 37 | // Raw JSON input 38 | if (input.startsWith("[") && input.endsWith("]")) { 39 | try { 40 | return JSON.parse(input) as KvKey; 41 | } catch (e) { 42 | console.error(e); 43 | return []; 44 | } 45 | } 46 | 47 | const result = [] as KvKeyPart[]; 48 | 49 | for (let part of input.split(",")) { 50 | const trimmed = part.trim(); 51 | result.push(evalSinglePart(trimmed)); 52 | } 53 | return result; 54 | } 55 | 56 | export function kvKeyToString(key: KvKey): string { 57 | return key.map((i) => i.toString()).join(","); 58 | } 59 | 60 | export type ValueType = "string" | "json" | "number"; 61 | 62 | export interface ValueCheckResult { 63 | isValid: boolean; 64 | reason: string; 65 | } 66 | export function isValidValueType( 67 | value: unknown, 68 | valueType: string, 69 | ): ValueCheckResult { 70 | if (valueType === "string") { 71 | return { 72 | isValid: true, 73 | reason: "string is always valid", 74 | }; 75 | } 76 | if (valueType === "number") { 77 | if (value === null || value === undefined) { 78 | return { 79 | isValid: false, 80 | reason: "number cannot be null", 81 | }; 82 | } 83 | if (Number.isNaN(parseFloat(value as string))) { 84 | return { 85 | isValid: false, 86 | reason: "invalid number", 87 | }; 88 | } 89 | return { 90 | isValid: true, 91 | reason: "OK", 92 | }; 93 | } 94 | if (valueType === "json") { 95 | if (value === null) { 96 | return { 97 | isValid: false, 98 | reason: "json cannot be null", 99 | }; 100 | } 101 | try { 102 | JSON.parse(value as string); 103 | } catch (e) { 104 | return { 105 | isValid: false, 106 | reason: "invalid json", 107 | }; 108 | } 109 | return { 110 | isValid: true, 111 | reason: "OK", 112 | }; 113 | } 114 | return { 115 | isValid: false, 116 | reason: "unknown valueType", 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /kv/src/main.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | 4 | import React, { useEffect, useState } from "react"; 5 | import { render } from "react-dom"; 6 | import { PageList } from "./list"; 7 | import { PageSingle } from "./single"; 8 | import { IconDatabase } from "./icons"; 9 | import { Config, getConfig, KvKey, kvRequestChangeDatabase } from "./api"; 10 | import { CSSTransition } from "react-transition-group"; 11 | 12 | // This script will be run within the webview itself 13 | // It cannot access the main VS Code APIs directly. 14 | 15 | export type PageType = "list" | "new" | "single"; 16 | 17 | (function () { 18 | interface DatabaseProps { 19 | database: string; 20 | onChangeDatabase: (database: string) => void; 21 | } 22 | 23 | function Database(props: DatabaseProps) { 24 | const { database } = props; 25 | let databaseName = "Default database"; 26 | 27 | if (database.startsWith("https://api.deno.com/databases/")) { 28 | databaseName = "Remote database"; 29 | } 30 | 31 | return ( 32 |
{ 35 | const result = await kvRequestChangeDatabase(); 36 | if (result === null) { 37 | console.log("User cancelled database change"); 38 | } 39 | if (result !== null) { 40 | props.onChangeDatabase(result); 41 | } 42 | }} 43 | > 44 | 45 |
46 | {databaseName} 47 |
48 |
49 | ); 50 | } 51 | 52 | function Page() { 53 | const [page, setPage] = useState("list"); 54 | const [prefix, setPrefix] = useState([]); 55 | const [selectedKey, setSelectedKey] = useState([]); 56 | const [database, setDatabase] = useState(""); 57 | const [config, setConfig] = useState(null); 58 | 59 | const showModal = page === "new" || page === "single"; 60 | 61 | useEffect(() => { 62 | (async () => { 63 | setConfig(await getConfig()); 64 | })(); 65 | }, []); 66 | 67 | return ( 68 | config && ( 69 |
70 | {page === "list" && ( 71 | { 77 | setSelectedKey(key); 78 | setPage("single"); 79 | }} 80 | onChangePrefix={(prefix) => { 81 | setPrefix(prefix); 82 | }} 83 | onChangePage={(page) => setPage(page)} 84 | config={config} 85 | /> 86 | )} 87 | 88 |
89 | {page === "new" && ( 90 | setPage(page)} 93 | onSaveNewItem={(key, value) => { 94 | setSelectedKey(key); 95 | setPage("single"); 96 | }} 97 | /> 98 | )} 99 | {page === "single" && ( 100 | setPage(page)} 102 | selectedKey={selectedKey} 103 | /> 104 | )} 105 |
106 |
107 | { 110 | setDatabase(result); 111 | }} 112 | /> 113 |
114 | ) 115 | ); 116 | } 117 | 118 | render( 119 | , 120 | document.getElementById("app"), 121 | ); 122 | })(); 123 | -------------------------------------------------------------------------------- /scripts/kv/server.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // @ts-nocheck 3 | 4 | import "https://deno.land/std@0.203.0/dotenv/load.ts"; 5 | import { load } from "https://deno.land/std@0.203.0/dotenv/mod.ts"; 6 | import superjson from "npm:superjson@1.13.3"; 7 | 8 | let db = await Deno.openKv(); 9 | 10 | // DB can take URL: 11 | // https://api.deno.com/databases/[UUID]/connect 12 | 13 | type MessageType = "list" | "changeDatabase" | "get" | "set" | "delete"; 14 | 15 | interface RequestJson { 16 | type: MessageType; 17 | key?: Deno.KvKey; 18 | value?: unknown; 19 | database?: string; 20 | } 21 | 22 | const handler = async (request: Request): Promise => { 23 | const method = request.method; 24 | 25 | if (method === "OPTIONS") { 26 | return new Response("", { 27 | headers: { 28 | "Access-Control-Allow-Origin": "*", 29 | "Access-Control-Allow-Headers": "Content-Type", 30 | "Access-Control-Allow-Methods": "POST, OPTIONS", 31 | }, 32 | }); 33 | } 34 | 35 | if (method !== "POST") { 36 | return new Response("Only POST is supported", { status: 400 }); 37 | } 38 | 39 | const url = new URL(request.url); 40 | const body = await request.text(); 41 | 42 | const { type, key, value, database, limit, cursor } = superjson.parse< 43 | RequestJson 44 | >(body); 45 | 46 | if (type === "list") { 47 | try { 48 | const keys = db.list({ 49 | prefix: key ?? [], 50 | }, { 51 | limit, 52 | cursor, 53 | }); 54 | 55 | const result = []; 56 | for await (const key of keys) { 57 | result.push(key); 58 | } 59 | 60 | return new Response( 61 | superjson.stringify({ result, cursor: keys.cursor }), 62 | { status: 200 }, 63 | ); 64 | } catch (e) { 65 | return new Response( 66 | "Failed to list items: " + e.message, 67 | { status: 500 }, 68 | ); 69 | } 70 | } 71 | // http://localhost:8080/?type=set&key=foo,bar&value=hello 72 | if (type === "set" && key) { 73 | try { 74 | await db.set(key, value); 75 | const result = { 76 | result: "OK", 77 | }; 78 | return new Response(superjson.stringify(result), { status: 200 }); 79 | } catch (e) { 80 | return new Response( 81 | "Failed to set item: " + e.message, 82 | { status: 500 }, 83 | ); 84 | } 85 | } 86 | 87 | // http://localhost:8080/?type=get&key=foo,bar 88 | if (type === "get" && key) { 89 | try { 90 | const value = await db.get(key); 91 | return new Response(superjson.stringify(value), { status: 200 }); 92 | } catch (e) { 93 | return new Response( 94 | "Failed to get item: " + e.message, 95 | { status: 500 }, 96 | ); 97 | } 98 | } 99 | 100 | if (type === "delete" && key) { 101 | try { 102 | await db.delete(key); 103 | const result = { 104 | result: "OK", 105 | }; 106 | return new Response(superjson.stringify(result), { status: 200 }); 107 | } catch (e) { 108 | return new Response( 109 | "Failed to delete item: " + e.message, 110 | { status: 500 }, 111 | ); 112 | } 113 | } 114 | 115 | if (type === "changeDatabase") { 116 | // Reload .env to get latest DENO_KV_ACCESS_TOKEN 117 | load(); 118 | 119 | try { 120 | if (database) { 121 | db = await Deno.openKv(database); 122 | } else { 123 | db = await Deno.openKv(); 124 | } 125 | } catch (e) { 126 | return new Response( 127 | "Failed to change database: " + e.message, 128 | { status: 500 }, 129 | ); 130 | } 131 | 132 | const result = { 133 | result: "OK", 134 | database: database, 135 | }; 136 | return new Response(JSON.stringify(result), { status: 200 }); 137 | } 138 | 139 | return new Response(`KV Viewer Server`, { status: 200 }); 140 | }; 141 | 142 | Deno.serve({ 143 | port: 0, 144 | onListen(s) { 145 | console.log(`Listening on port ${s.port}`); 146 | }, 147 | }, handler); 148 | -------------------------------------------------------------------------------- /media/kv/main.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. */ 2 | 3 | body, 4 | html { 5 | background-color: transparent; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | #app { 12 | display: flex; 13 | flex: 1; 14 | flex-direction: column; 15 | height: 100%; 16 | overflow-y: hidden; 17 | } 18 | 19 | .nav { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | gap: 0.25rem; 24 | padding: 0.25rem; 25 | position: relative; 26 | } 27 | 28 | .nav__button { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | padding: 0.25rem; 33 | max-width: max-content; 34 | cursor: pointer; 35 | background: transparent; 36 | } 37 | .nav__button:hover { 38 | background-color: var(--vscode-list-hoverBackground); 39 | border-radius: 0.25rem; 40 | } 41 | 42 | .nav__item { 43 | background: transparent; 44 | } 45 | .nav__item--selected { 46 | border-bottom: 2px solid #666; 47 | } 48 | .nav__back-home { 49 | } 50 | .nav__new-item { 51 | } 52 | .nav__openmenu { 53 | } 54 | 55 | .nav__title { 56 | font-size: 1em; 57 | font-weight: bold; 58 | flex: 1; 59 | } 60 | 61 | .form__wrapper { 62 | display: flex; 63 | } 64 | 65 | .form__query { 66 | flex: 1; 67 | } 68 | 69 | .form__submit { 70 | width: 2rem; 71 | display: flex; 72 | align-items: center; 73 | justify-content: center; 74 | } 75 | .page { 76 | display: flex; 77 | flex-direction: column; 78 | flex: 1; 79 | overflow-y: hidden; 80 | overflow-x: hidden; 81 | } 82 | 83 | .result__wrapper { 84 | flex: 1; 85 | display: flex; 86 | flex-direction: column; 87 | overflow-y: hidden; 88 | } 89 | 90 | .result { 91 | flex: 1; 92 | overflow-x: hidden; 93 | } 94 | .result__item { 95 | border-bottom: 1px solid #666; 96 | padding: 0.5rem 0.25rem; 97 | overflow-x: hidden; 98 | white-space: nowrap; 99 | } 100 | .result__item:hover { 101 | background-color: var(--vscode-list-hoverBackground); 102 | cursor: pointer; 103 | } 104 | 105 | .result__item__key { 106 | display: inline; 107 | } 108 | 109 | .key--string { 110 | } 111 | .key--number { 112 | color: #d59bff; 113 | } 114 | /* .result__item__key__strict { 115 | display: none; 116 | } 117 | 118 | .result__item:hover .result__item__key__strict { 119 | display: inline; 120 | } 121 | .result__item:hover .result__item__key__simple { 122 | display: none; 123 | } */ 124 | 125 | .result__item__value { 126 | margin-left: 0.5rem; 127 | display: inline; 128 | text-overflow: ellipsis; 129 | white-space: nowrap; 130 | color: rgb(152, 166, 194); 131 | } 132 | 133 | .new__wrapper { 134 | overflow-x: hidden; 135 | flex: 1; 136 | } 137 | 138 | .single__wrapper { 139 | flex: 1; 140 | } 141 | .icon { 142 | flex: none; 143 | } 144 | 145 | .newform__wrapper { 146 | } 147 | 148 | .label { 149 | font-weight: bold; 150 | font-size: 0.75rem; 151 | margin-top: 0.75rem; 152 | margin-bottom: 0.25rem; 153 | } 154 | 155 | .single__value { 156 | margin-top: 1rem; 157 | } 158 | 159 | .single__value__type { 160 | background-color: transparent; 161 | color: white; 162 | padding: 0.25rem; 163 | } 164 | 165 | .single__value-checker { 166 | flex: 1; 167 | display: flex; 168 | flex-direction: row; 169 | align-items: center; 170 | } 171 | 172 | .message--error { 173 | color: #eb3e3e; 174 | } 175 | .message--success { 176 | color: #87e587; 177 | } 178 | 179 | textarea.single__value__textarea { 180 | height: 10rem; 181 | flex: 1; 182 | padding: 0.25rem; 183 | color: #c2c1ff; 184 | } 185 | 186 | .single__value__wrapper { 187 | display: flex; 188 | flex-direction: row; 189 | flex: 1; 190 | } 191 | 192 | .single__key { 193 | display: flex; 194 | flex-direction: row; 195 | flex: 1; 196 | } 197 | 198 | .single__key__textarea { 199 | flex: 1; 200 | font-size: 110%; 201 | color: #e9e4ca; 202 | } 203 | 204 | .value-column { 205 | display: flex; 206 | flex-direction: row; 207 | flex: 1; 208 | } 209 | 210 | .value-column .label { 211 | flex: 1; 212 | } 213 | 214 | .single__key__textarea:read-only { 215 | border: 1px solid #666; 216 | border-radius: 4px; 217 | background-color: transparent; 218 | } 219 | .single__key__textarea:focus { 220 | /* background-color: var(--vscode-inputValidation-infoBackground); */ 221 | } 222 | 223 | .database__wrapper { 224 | color: #d199ff; 225 | display: flex; 226 | align-items: center; 227 | gap: 0.25rem; 228 | cursor: pointer; 229 | } 230 | 231 | .database__wrapper:hover { 232 | background-color: var(--vscode-list-hoverBackground); 233 | } 234 | .database { 235 | padding: 0.5rem 0rem; 236 | } 237 | 238 | .modal { 239 | display: flex; 240 | flex-direction: column; 241 | } 242 | 243 | .modal-enter { 244 | opacity: 0; 245 | transform: translateX(50%); 246 | } 247 | .modal-enter-active { 248 | opacity: 1; 249 | flex: 1; 250 | transform: translateX(0); 251 | transition: opacity 600ms, transform 200ms ease-out; 252 | } 253 | .modal-enter-done { 254 | flex: 1; 255 | } 256 | 257 | .single__versionstamp { 258 | font-family: monospace; 259 | font-size: 90%; 260 | color: #959595; 261 | } 262 | 263 | span.doc { 264 | margin-left: 0.25rem; 265 | } 266 | 267 | span.doc a { 268 | font-weight: normal; 269 | color: #959595; 270 | } 271 | 272 | .nav__menu { 273 | position: absolute; 274 | display: none; 275 | top: 100%; 276 | right: 0; 277 | background-color: var(--vscode-editor-background); 278 | border: 1px solid var(--vscode-editorWidget-border); 279 | border-radius: 0.25rem; 280 | padding: 0.25rem; 281 | } 282 | .nav__menu--open { 283 | display: block; 284 | } 285 | .nav__menu__item { 286 | display: flex; 287 | flex-direction: row; 288 | align-items: center; 289 | margin-bottom: 0.25rem; 290 | padding: 0.25rem; 291 | cursor: pointer; 292 | } 293 | .nav__menu__item:hover { 294 | background-color: var(--vscode-list-hoverBackground); 295 | border-radius: 0.25rem; 296 | } 297 | 298 | .result__item__key__badge { 299 | margin-left: 0.25rem; 300 | padding: 0.25rem; 301 | border-radius: 0.25rem; 302 | background-color: #333; 303 | color: white; 304 | font-size: 0.75rem; 305 | } 306 | 307 | .result__item__key__badge--string { 308 | color: #87e587; 309 | } 310 | -------------------------------------------------------------------------------- /kv/src/icons.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | 4 | import React from "react"; 5 | import cx from "classnames"; 6 | 7 | export function IconDatabase(props: React.SVGProps) { 8 | return ( 9 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export function IconSearch(props: React.SVGProps) { 31 | return ( 32 | 40 | 44 | 45 | ); 46 | } 47 | 48 | export function IconPlus(props: React.SVGProps) { 49 | return ( 50 | 61 | 62 | 63 | 64 | 65 | ); 66 | } 67 | 68 | export function IconChevronRight(props: React.SVGProps) { 69 | return ( 70 | 78 | 79 | 86 | 87 | ); 88 | } 89 | 90 | export function IconChevronLeft(props: React.SVGProps) { 91 | return ( 92 | 100 | 101 | 108 | 109 | ); 110 | } 111 | 112 | export function IconDots(props: React.SVGProps) { 113 | return ( 114 | 122 | 123 | 132 | 141 | 150 | 151 | ); 152 | } 153 | 154 | export function IconTrash(props: React.SVGProps) { 155 | return ( 156 | 164 | 165 | 172 | 179 | 186 | 193 | 194 | ); 195 | } 196 | 197 | export function Spinner(props: React.SVGProps) { 198 | return ( 199 | 206 | 210 | 217 | 218 | 219 | ); 220 | } 221 | -------------------------------------------------------------------------------- /kv/src/list.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | 4 | import React, { useEffect, useRef, useState } from "react"; 5 | import { 6 | Config, 7 | getConfig, 8 | KvKey, 9 | kvList, 10 | KvPair, 11 | showMessage, 12 | vscode, 13 | } from "./api"; 14 | import { IconSearch, Spinner } from "./icons"; 15 | import { kvKeyToString, queryToKvPrefix } from "./utils"; 16 | import { Nav, NewItem } from "./nav"; 17 | import { PageType } from "./main"; 18 | import superjson from "superjson"; 19 | 20 | interface PageListFormProps { 21 | prefix: KvKey; 22 | onSubmit: (key: string) => void; 23 | isBusy: boolean; 24 | } 25 | 26 | function PageListForm(props: PageListFormProps) { 27 | const searchKeyRef = useRef(null); 28 | 29 | useEffect(() => { 30 | if (searchKeyRef.current === null) { 31 | return; 32 | } 33 | searchKeyRef.current.focus(); 34 | }, [searchKeyRef]); 35 | 36 | const keyString = kvKeyToString(props.prefix); 37 | 38 | return ( 39 |
{ 42 | e.preventDefault(); 43 | if (searchKeyRef.current === null) { 44 | return; 45 | } 46 | const searchKey = searchKeyRef.current.value; 47 | props.onSubmit(searchKey); 48 | }} 49 | > 50 | 57 | 62 |
63 | ); 64 | } 65 | 66 | interface PageListResultItemProps { 67 | item: { 68 | key: KvKey; 69 | value: string; 70 | }; 71 | onChangeSelectedKey: (key: KvKey) => void; 72 | previewValue?: boolean; 73 | } 74 | function PageListResultItem(props: PageListResultItemProps) { 75 | const item = props.item; 76 | 77 | return ( 78 |
{ 81 | props.onChangeSelectedKey(item.key); 82 | }} 83 | > 84 |
85 | { 86 | 87 | {item.key.map((i) => { 88 | const isString = typeof i === "string"; 89 | return isString 90 | ? ( 91 | 92 | "{i}" 93 | 94 | ) 95 | : ( 96 | 97 | {i.toString()} 98 | 99 | ); 100 | })} 101 | 102 | } 103 |
104 | {props.previewValue && ( 105 |
106 | {JSON.stringify(item.value)} 107 |
108 | )} 109 |
110 | ); 111 | } 112 | 113 | interface PageListResultProps { 114 | items: KvPair[]; 115 | onChangeSelectedKey: (key: KvKey) => void; 116 | previewValue?: boolean; 117 | onLoadMore: () => void; 118 | cursor: string | null; 119 | } 120 | function PageListResult(props: PageListResultProps) { 121 | const items = props.items; 122 | 123 | return ( 124 |
125 | {items.length === 0 && ( 126 |
127 | No items found 128 |
129 | )} 130 | {items.map((item) => ( 131 | props.onChangeSelectedKey(key)} 135 | previewValue={props.previewValue} 136 | /> 137 | ))} 138 | {props.cursor && ( 139 | 142 | )} 143 |
144 | ); 145 | } 146 | 147 | interface PageListProps { 148 | database: string; 149 | onChangeSelectedKey: (key: KvKey) => void; 150 | prefix: KvKey; 151 | onChangePrefix: (prefix: KvKey) => void; 152 | onChangePage: (page: PageType) => void; 153 | config: Config; 154 | } 155 | 156 | function getExampleCode(selectedKey: KvKey) { 157 | return `const kv = await Deno.openKv(); 158 | 159 | const cur = kv.list({ prefix: ${JSON.stringify(selectedKey)}}); 160 | const result = []; 161 | 162 | for await (const entry of cur) { 163 | console.log(entry.key); // ["preferences", "ada"] 164 | console.log(entry.value); // { ... } 165 | console.log(entry.versionstamp); // "00000000000000010000" 166 | result.push(entry); 167 | }`; 168 | } 169 | 170 | export function PageList(props: PageListProps) { 171 | const [items, setItems] = useState([]); 172 | const [isBusy, setIsBusy] = useState(false); 173 | const [cursor, setCursor] = useState(null); 174 | 175 | const menus = [{ 176 | title: "Copy code with kv.list", 177 | onClick: () => { 178 | navigator.clipboard.writeText(getExampleCode(props.prefix ?? [])); 179 | showMessage("Copied!"); 180 | }, 181 | }, { 182 | title: "Export JSON", 183 | onClick: () => { 184 | const blob = new Blob([JSON.stringify(items, null, 2)], { 185 | type: "application/json", 186 | }); 187 | const url = URL.createObjectURL(blob); 188 | const a = document.createElement("a"); 189 | a.href = url; 190 | a.download = "kv.json"; 191 | a.click(); 192 | }, 193 | }]; 194 | 195 | async function loadMore(cursor: string | null) { 196 | const { result, cursor: cursorResult } = await kvList( 197 | props.prefix ?? [], 198 | props.config.listFetchSize, 199 | cursor ?? "", 200 | ); 201 | setItems((prev) => [...prev, ...result]); 202 | setCursor(cursorResult); 203 | } 204 | 205 | useEffect(() => { 206 | setIsBusy(true); 207 | setItems([]); 208 | setCursor(null); 209 | (async () => { 210 | await loadMore(null); 211 | setIsBusy(false); 212 | })(); 213 | }, [props.prefix]); 214 | 215 | return ( 216 | <> 217 | 225 |
226 | { 230 | const parsed = queryToKvPrefix(key); 231 | props.onChangePrefix(parsed); 232 | }} 233 | /> 234 | {items && ( 235 | { 238 | props.onChangeSelectedKey(key); 239 | }} 240 | previewValue={props.config.previewValue} 241 | cursor={cursor} 242 | onLoadMore={() => loadMore(cursor)} 243 | /> 244 | )} 245 |
246 | 247 | ); 248 | } 249 | -------------------------------------------------------------------------------- /src/kv/webview.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | 4 | import * as vscode from "vscode"; 5 | import fetch from "node-fetch"; 6 | import superjson from "superjson"; 7 | 8 | export type KvKeyPart = Uint8Array | string | number | bigint | boolean; 9 | export type KvKey = KvKeyPart[]; 10 | 11 | type MessageType = 12 | | "list" 13 | | "changeDatabase" 14 | | "get" 15 | | "set" 16 | | "delete"; 17 | 18 | interface ResponseJson { 19 | type: MessageType; 20 | result: unknown; 21 | database?: string; 22 | } 23 | 24 | export class KvViewProvider implements vscode.WebviewViewProvider { 25 | private _view?: vscode.WebviewView; 26 | constructor( 27 | private readonly _extensionUri: vscode.Uri, 28 | private readonly _port: string, 29 | ) {} 30 | resolveWebviewView( 31 | webviewView: vscode.WebviewView, 32 | _context: vscode.WebviewViewResolveContext, 33 | _token: vscode.CancellationToken, 34 | ): void | Thenable { 35 | this._view = webviewView; 36 | 37 | webviewView.webview.options = { 38 | enableScripts: true, 39 | 40 | localResourceRoots: [ 41 | this._extensionUri, 42 | ], 43 | }; 44 | 45 | webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); 46 | 47 | webviewView.webview.onDidReceiveMessage(async (data) => { 48 | const type = data.type; // list, get, set, delete/ database 49 | const key = data.key; 50 | const value = data.value; 51 | const database = data.database; 52 | const limit = data.limit; 53 | const cursor = data.cursor; 54 | const id = data.id; 55 | 56 | if (type === "message") { 57 | vscode.window.showInformationMessage(data.message); 58 | return; 59 | } 60 | 61 | const url = `http://localhost:${this._port}/`; 62 | 63 | const requestJson = { 64 | type, 65 | key, 66 | value, 67 | database, 68 | limit, 69 | cursor, 70 | }; 71 | 72 | if (type === "changeDatabase") { 73 | const db = await vscode.window.showInputBox({ 74 | prompt: 75 | "Enter KV database file path / URL / UUID (Empty to use default))", 76 | value: database, 77 | }); 78 | if (db === undefined) { 79 | webviewView.webview.postMessage({ 80 | id, 81 | type: "changeDatabaseResult", 82 | result: null, 83 | }); 84 | } else { 85 | // URL form is : 86 | // https://api.deno.com/databases/[UUID]/connect 87 | 88 | const uuidRegex = /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i; 89 | if (uuidRegex.test(db)) { 90 | requestJson.database = 91 | `https://api.deno.com/databases/${db}/connect`; 92 | } else { 93 | requestJson.database = db; 94 | } 95 | } 96 | } 97 | 98 | if (type === "getConfig") { 99 | const previewValue: boolean = vscode.workspace.getConfiguration("kivi") 100 | .get("previewValue") as boolean; 101 | const listFetchSize: number = vscode.workspace.getConfiguration("kivi") 102 | .get("listFetchSize") as number; 103 | 104 | webviewView.webview.postMessage({ 105 | id, 106 | type: "getConfigResult", 107 | result: JSON.stringify({ 108 | previewValue, 109 | listFetchSize, 110 | }), 111 | }); 112 | return; 113 | } 114 | 115 | const response = await fetch(url, { 116 | method: "POST", 117 | body: superjson.stringify(requestJson), 118 | headers: { 119 | "Content-Type": "application/json", 120 | }, 121 | }); 122 | if (!response.ok) { 123 | const errorMessage = await response.text(); 124 | vscode.window.showErrorMessage(`KV Viewer: ${errorMessage}`); 125 | 126 | throw new Error(errorMessage); 127 | } 128 | const result = superjson.parse(await response.text()); 129 | 130 | if (type === "list") { 131 | webviewView.webview.postMessage({ 132 | id, 133 | type: "listResult", 134 | result: superjson.stringify(result), 135 | }); 136 | } 137 | if (type === "set") { 138 | webviewView.webview.postMessage({ 139 | id, 140 | type: "setResult", 141 | result: result.result, 142 | }); 143 | } 144 | if (type === "get") { 145 | webviewView.webview.postMessage({ 146 | id, 147 | type: "getResult", 148 | result: superjson.stringify(result), 149 | }); 150 | } 151 | if (type === "delete") { 152 | webviewView.webview.postMessage({ 153 | id, 154 | type: "deleteResult", 155 | result: result.result, 156 | }); 157 | } 158 | if (type === "changeDatabase") { 159 | webviewView.webview.postMessage({ 160 | id, 161 | type: "changeDatabaseResult", 162 | result: requestJson.database, 163 | }); 164 | } 165 | if (type === "message") { 166 | webviewView.webview.postMessage({ 167 | id, 168 | type: "messageResult", 169 | result: "OK", 170 | }); 171 | } 172 | }); 173 | } 174 | 175 | private _getHtmlForWebview(webview: vscode.Webview) { 176 | // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. 177 | const subFolder = "kv"; 178 | const scriptUri = webview.asWebviewUri( 179 | vscode.Uri.joinPath( 180 | this._extensionUri, 181 | "media", 182 | subFolder, 183 | "main.js", 184 | ), 185 | ); 186 | 187 | // Do the same for the stylesheet. 188 | const styleVSCodeUri = webview.asWebviewUri( 189 | vscode.Uri.joinPath( 190 | this._extensionUri, 191 | "media", 192 | subFolder, 193 | "vscode.css", 194 | ), 195 | ); 196 | const styleMainUri = webview.asWebviewUri( 197 | vscode.Uri.joinPath( 198 | this._extensionUri, 199 | "media", 200 | subFolder, 201 | "main.css", 202 | ), 203 | ); 204 | 205 | const nonce = getNonce(); 206 | 207 | return ` 208 | 209 | 210 | 211 | 212 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | Fresh URL Matcher 225 | 226 | 227 |
228 |
229 | 230 | 231 | 232 | `; 233 | } 234 | } 235 | 236 | function getNonce() { 237 | let text = ""; 238 | const possible = 239 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 240 | for (let i = 0; i < 32; i++) { 241 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 242 | } 243 | return text; 244 | } 245 | -------------------------------------------------------------------------------- /kv/src/single.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | 4 | import React, { useEffect, useState } from "react"; 5 | import { kvDelete, kvGet, KvKey, kvSet } from "./api"; 6 | import { isValidValueType, queryToKvPrefix, ValueType } from "./utils"; 7 | import { BackHome, Nav, NewItem } from "./nav"; 8 | import { PageType } from "./main"; 9 | 10 | interface PageSingleProps { 11 | selectedKey?: KvKey; 12 | isNewItem?: boolean; 13 | onSaveNewItem?: (key: KvKey, value: unknown) => void; 14 | onChangePage: (page: PageType) => void; 15 | } 16 | export function PageSingle(props: PageSingleProps) { 17 | const [selectedKey, setSelectedKey] = useState( 18 | props.selectedKey, 19 | ); 20 | const [value, setValue] = useState(null); 21 | const [isNewItem, _] = useState(props.isNewItem || false); 22 | const [newKey, setNewKey] = useState(props.selectedKey); 23 | const [versionstamp, setVersionstamp] = useState(null); 24 | interface Message { 25 | message: string; 26 | level: "success" | "info" | "error"; 27 | } 28 | 29 | const [message, setMessage] = useState(null); 30 | const [valueType, setValueType] = useState("string"); 31 | 32 | useEffect(() => { 33 | if (selectedKey) { 34 | kvGet(selectedKey).then((result) => { 35 | const value = result.value; 36 | let valueType: ValueType = "string"; 37 | if (typeof value === "object") { 38 | valueType = "json"; 39 | setValue(JSON.stringify(value, null, 2)); 40 | } else if (typeof value === "number") { 41 | valueType = "number"; 42 | setValue(String(value)); 43 | } else { 44 | setValue(JSON.stringify(value)); 45 | } 46 | setValueType(valueType); 47 | setVersionstamp(result.versionstamp); 48 | }); 49 | } 50 | }, [selectedKey]); 51 | 52 | const getExample = `const kv = await Deno.openKv(); 53 | 54 | const res = await kv.get([${JSON.stringify(newKey || [])}]); 55 | console.log(res); // value`; 56 | 57 | const setExample = `const kv = await Deno.openKv(); 58 | 59 | const res = await kv.set(${JSON.stringify(newKey || [])}, ${value || ""}); 60 | console.log(res.versionstamp);`; 61 | 62 | const menus = [{ 63 | title: "Delete this item", 64 | onClick: async () => { 65 | if (!selectedKey) { 66 | return; 67 | } 68 | const result = await kvDelete(selectedKey); 69 | if (result === "OK") { 70 | setMessage({ 71 | message: "The item deleted successfully : " + new Date(), 72 | level: "success", 73 | }); 74 | } 75 | }, 76 | }, { 77 | title: "Copy code with kv.get", 78 | onClick: () => { 79 | navigator.clipboard.writeText(getExample); 80 | }, 81 | }, { 82 | title: "Copy code with kv.set", 83 | onClick: () => { 84 | navigator.clipboard.writeText(setExample); 85 | }, 86 | }]; 87 | 88 | const newItem = ( 89 | <> 90 | props.onChangePage("list")} 92 | /> 93 |
94 | New Item 95 |
96 | 97 | ); 98 | const editItem = ( 99 | <> 100 | props.onChangePage("list")} 102 | /> 103 |
104 | Edit Item 105 |
106 | props.onChangePage("new")} 108 | /> 109 | 110 | ); 111 | 112 | return ( 113 | <> 114 | 120 |
121 |
122 | Key 123 | 124 | doc 125 | 126 |
127 | 128 |
129 |