From 83091e74bb96724bfaf6b6221c13ef24da52b4ae Mon Sep 17 00:00:00 2001 From: alexweininger Date: Sat, 29 May 2021 09:32:57 -0500 Subject: [PATCH] Detailed function support, create tag still broken --- package-lock.json | 80 ++++++- package.json | 165 ++++++++++++++- src/appwrite.d.ts | 7 +- src/appwrite/Functions.ts | 2 +- src/appwrite/Storage.ts | 5 + src/commands/common/editValue.ts | 9 + src/commands/functions/activateTag.ts | 7 + src/commands/functions/copyExecutionErrors.ts | 11 + src/commands/functions/copyExecutionOutput.ts | 11 + src/commands/functions/createFunction.ts | 19 ++ src/commands/functions/createFunctionVar.ts | 16 ++ src/commands/functions/createTag.ts | 30 ++- src/commands/functions/deleteFunction.ts | 9 + src/commands/functions/deleteFunctionVar.ts | 13 ++ src/commands/functions/deleteTag.ts | 11 + .../functions/openExecutionsInBrowser.ts | 15 ++ .../openFunctionSettingsInBrowser.ts | 15 ++ .../functions/openFunctionTagsInBrowser.ts | 13 ++ src/commands/functions/viewExecutionErrors.ts | 11 + src/commands/functions/viewExecutionOutput.ts | 11 + src/commands/openDocumentation.ts | 3 +- src/commands/registerCommands.ts | 34 ++- src/commands/users/openUserInConsole.ts | 2 +- src/constants.ts | 197 +++++++++++++++++- src/tree/common/EditableTreeItemBase.ts | 13 ++ src/tree/common/EnumEditableTreeItem.ts | 38 ++++ src/tree/common/SimpleEditableTreeItem.ts | 18 ++ src/tree/common/StringEditableTreeItem.ts | 22 ++ src/tree/functions/FunctionTreeItem.ts | 12 +- .../functions/FunctionsTreeItemProvider.ts | 5 +- .../{ => executions}/ExecutionTreeItem.ts | 35 +++- .../{ => executions}/ExecutionsTreeItem.ts | 8 +- src/tree/functions/settings/EventsTreeItem.ts | 31 +++ .../settings/FunctionSettingsTreeItem.ts | 45 ++++ src/tree/functions/settings/NameTreeItem.ts | 43 ++++ .../functions/settings/ScheduleTreeItem.ts | 44 ++++ .../functions/settings/TimeoutTreeItem.ts | 48 +++++ src/tree/functions/settings/VarTreeItem.ts | 64 ++++++ src/tree/functions/settings/VarsTreeItem.ts | 21 ++ src/tree/functions/{ => tags}/TagTreeItem.ts | 8 +- src/tree/functions/{ => tags}/TagsTreeItem.ts | 6 +- src/utils/date.ts | 5 + src/utils/tar.ts | 19 +- src/utils/validation.ts | 0 44 files changed, 1101 insertions(+), 80 deletions(-) create mode 100644 src/commands/common/editValue.ts create mode 100644 src/commands/functions/activateTag.ts create mode 100644 src/commands/functions/copyExecutionErrors.ts create mode 100644 src/commands/functions/copyExecutionOutput.ts create mode 100644 src/commands/functions/createFunction.ts create mode 100644 src/commands/functions/createFunctionVar.ts create mode 100644 src/commands/functions/deleteFunction.ts create mode 100644 src/commands/functions/deleteFunctionVar.ts create mode 100644 src/commands/functions/deleteTag.ts create mode 100644 src/commands/functions/openExecutionsInBrowser.ts create mode 100644 src/commands/functions/openFunctionSettingsInBrowser.ts create mode 100644 src/commands/functions/openFunctionTagsInBrowser.ts create mode 100644 src/commands/functions/viewExecutionErrors.ts create mode 100644 src/commands/functions/viewExecutionOutput.ts create mode 100644 src/tree/common/EditableTreeItemBase.ts create mode 100644 src/tree/common/EnumEditableTreeItem.ts create mode 100644 src/tree/common/SimpleEditableTreeItem.ts create mode 100644 src/tree/common/StringEditableTreeItem.ts rename src/tree/functions/{ => executions}/ExecutionTreeItem.ts (67%) rename src/tree/functions/{ => executions}/ExecutionsTreeItem.ts (79%) create mode 100644 src/tree/functions/settings/EventsTreeItem.ts create mode 100644 src/tree/functions/settings/FunctionSettingsTreeItem.ts create mode 100644 src/tree/functions/settings/NameTreeItem.ts create mode 100644 src/tree/functions/settings/ScheduleTreeItem.ts create mode 100644 src/tree/functions/settings/TimeoutTreeItem.ts create mode 100644 src/tree/functions/settings/VarTreeItem.ts create mode 100644 src/tree/functions/settings/VarsTreeItem.ts rename src/tree/functions/{ => tags}/TagTreeItem.ts (68%) rename src/tree/functions/{ => tags}/TagsTreeItem.ts (80%) create mode 100644 src/utils/date.ts create mode 100644 src/utils/validation.ts diff --git a/package-lock.json b/package-lock.json index 998151a..0df19f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,14 @@ } } }, + "@babel/runtime": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", + "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@discoveryjs/json-ext": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz", @@ -211,6 +219,11 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/lodash": { + "version": "4.14.170", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", + "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -928,6 +941,19 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cron-validate": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cron-validate/-/cron-validate-1.4.3.tgz", + "integrity": "sha512-N+qKw019oQBEPIP5Qwi8Z5XelQ00ThN6Maahwv+9UGu2u/b/MPb35zngMQI0T8pBoNiBrIXGlhvsmspNSYae/w==", + "requires": { + "yup": "0.32.9" + } + }, + "cronstrue": { + "version": "1.113.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-1.113.0.tgz", + "integrity": "sha512-j0+CQsQx0g0Iv6nQs0bHkLcpeCzYShWUdQ3QwSHV+dUyTLqI/3NPrHceeDfTXmC3Re4osMli5+wAYpffNO+e9w==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1421,9 +1447,9 @@ "dev": true }, "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" }, "form-data": { "version": "4.0.0", @@ -1950,8 +1976,12 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash.clonedeep": { "version": "4.5.0", @@ -2191,6 +2221,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -2210,9 +2245,9 @@ "dev": true }, "node-appwrite": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-2.2.1.tgz", - "integrity": "sha512-YbdcJJo4GD3v2rwChUz7uEMJQyzV6fbGdjLj2eshsK0ynqK76GB5M513Qs5E8cid50i4KFbFL9B1uV8oaQ/PAQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-2.2.3.tgz", + "integrity": "sha512-2j7AIKUxbjN25QrqZfMBRuWVRYlB5fixmW0HF/XP5QnrttCfozjPa5wWrgVRrJLYCoqwe2wwgWc9S3fyZeP/0g==", "requires": { "axios": "^0.21.1", "form-data": "^4.0.0" @@ -2481,6 +2516,11 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -2567,6 +2607,11 @@ "resolve": "^1.9.0" } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", @@ -2947,6 +2992,11 @@ "is-number": "^7.0.0" } }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -3460,6 +3510,20 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "yup": { + "version": "0.32.9", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz", + "integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==", + "requires": { + "@babel/runtime": "^7.10.5", + "@types/lodash": "^4.14.165", + "lodash": "^4.17.20", + "lodash-es": "^4.17.15", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } } } } diff --git a/package.json b/package.json index a8eafeb..1c64313 100644 --- a/package.json +++ b/package.json @@ -186,6 +186,11 @@ "title": "Refresh projects", "icon": "$(refresh)" }, + { + "command": "vscode-appwrite.refreshFunctions", + "title": "Refresh functions", + "icon": "$(refresh)" + }, { "command": "vscode-appwrite.removeProject", "title": "Remove project", @@ -196,10 +201,84 @@ "title": "Create function tag", "icon": "$(cloud-upload)" }, + { + "command": "vscode-appwrite.deleteTag", + "title": "Delete tag", + "icon": "$(trash)" + }, { "command": "vscode-appwrite.CreateExecution", - "title": "Execute function", - "icon": "$(play)" + "title": "Execute" + }, + { + "command": "vscode-appwrite.activateTag", + "title": "Activate" + }, + { + "command": "vscode-appwrite.editValue", + "title": "Edit", + "icon": "$(edit)" + }, + { + "command": "vscode-appwrite.deleteFunction", + "title": "Delete", + "icon": "$(trash)" + }, + { + "command": "vscode-appwrite.openFunctionsDocumentation", + "title": "Open functions documentation", + "icon": "$(book)" + }, + { + "command": "vscode-appwrite.createFunction", + "title": "Create function", + "icon": "$(add)" + }, + { + "command": "vscode-appwrite.createFunctionVar", + "title": "Create variable", + "icon": "$(add)" + }, + { + "command": "vscode-appwrite.deleteFunctionVar", + "title": "Delete variable", + "icon": "$(trash)" + }, + { + "command": "vscode-appwrite.viewExecutionOutput", + "title": "View execution stdout", + "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/" + }, + { + "command": "vscode-appwrite.viewExecutionErrors", + "title": "View execution stderr", + "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/" + }, + { + "command": "vscode-appwrite.copyExecutionOutput", + "title": "Copy execution stdout", + "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/" + }, + { + "command": "vscode-appwrite.copyExecutionErrors", + "title": "Copy execution stderr", + "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/" + }, + { + "command": "vscode-appwrite.openExecutionsInBrowser", + "title": "View executions in browser", + "enablement": "viewItem =~ /^(executions)$/", + "icon": "$(link-external)" + }, + { + "command": "vscode-appwrite.openFunctionTagsInBrowser", + "title": "Open function tags in browser", + "icon": "$(link-external)" + }, + { + "command": "vscode-appwrite.openFunctionSettingsInBrowser", + "title": "Open function settings in browser", + "icon": "$(link-external)" } ], "views": { @@ -292,14 +371,24 @@ "when": "view == Storage", "group": "navigation" }, + { + "command": "vscode-appwrite.openFunctionsDocumentation", + "when": "view == Functions", + "group": "navigation" + }, { "command": "vscode-appwrite.refreshProjects", "when": "view == Projects", "group": "navigation" }, { - "command": "vscode-appwrite.addProject", - "when": "view == Projects", + "command": "vscode-appwrite.refreshFunctions", + "when": "view == Functions", + "group": "navigation" + }, + { + "command": "vscode-appwrite.createFunction", + "when": "view == Functions", "group": "navigation" } ], @@ -402,7 +491,69 @@ }, { "command": "vscode-appwrite.CreateExecution", - "when": "viewItem =~ /(function)/", + "when": "viewItem =~ /^(function)$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.activateTag", + "when": "viewItem =~ /^(tag)$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.editValue", + "when": "viewItem =~ /^(editable)/", + "group": "inline" + }, + { + "command": "vscode-appwrite.deleteFunction", + "when": "viewItem =~ /^(function)$/" + }, + { + "command": "vscode-appwrite.deleteFunctionVar", + "when": "viewItem =~ /(var)$/" + }, + { + "command": "vscode-appwrite.createFunctionVar", + "when": "viewItem =~ /^(vars)$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.deleteTag", + "when": "viewItem =~ /^(tag)$/" + }, + { + "command": "vscode-appwrite.viewExecutionErrors", + "when": "viewItem =~ /^execution[^s]*$/", + "group": "view@1" + }, + { + "command": "vscode-appwrite.viewExecutionOutput", + "when": "viewItem =~ /^execution[^s]*$/", + "group": "view@1" + }, + { + "command": "vscode-appwrite.copyExecutionErrors", + "when": "viewItem =~ /^execution[^s]*$/", + "group": "copy@2" + }, + { + "command": "vscode-appwrite.copyExecutionOutput", + "when": "viewItem =~ /^execution[^s]*$/", + "group": "copy@2" + }, + { + "command": "vscode-appwrite.openExecutionsInBrowser", + "when": "viewItem =~ /^executions$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.openFunctionTagsInBrowser", + "when": "viewItem =~ /^tags$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.openFunctionSettingsInBrowser", + "when": "viewItem =~ /^functionSettings$/", "group": "inline" } ], @@ -542,9 +693,11 @@ "webpack-cli": "^4.4.0" }, "dependencies": { + "cron-validate": "^1.4.3", + "cronstrue": "^1.113.0", "dayjs": "^1.10.4", "fs-extra": "^9.1.0", - "node-appwrite": "^2.2.1", + "node-appwrite": "^2.2.3", "tar": "^6.1.0" } } diff --git a/src/appwrite.d.ts b/src/appwrite.d.ts index 5008098..85c16cb 100644 --- a/src/appwrite.d.ts +++ b/src/appwrite.d.ts @@ -359,7 +359,7 @@ export type AppwriteHealth = { }; export type StorageClient = { - createFile: (file: any, read: string[], write: string[]) => Promise; + createFile: (file: any, read?: string[], write?: string[]) => Promise; listFiles: () => Promise; getFile: (fileId: string) => Promise; }; @@ -431,7 +431,7 @@ export type FunctionsClient = { create: (name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise; list: (search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC') => Promise; get: (functionId: string) => Promise; - update: (functionId: string, name: string, execute: string, vars: Vars, events: string[], schedule?: string, timeout?: number) => Promise; + update: (functionId: string, name: string, execute: string[], vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise; updateTag: (functionId: string, tagId: string) => Promise; delete: (functionId: string) => Promise; createTag: (id: string, command: string, code: ReadStream) => Promise; @@ -443,9 +443,6 @@ export type FunctionsClient = { getExecution: (functionId: string, executionId: string) => Promise; } - - - export type SDK = { Client: new () => Client; diff --git a/src/appwrite/Functions.ts b/src/appwrite/Functions.ts index d96bc98..2fb0e6a 100644 --- a/src/appwrite/Functions.ts +++ b/src/appwrite/Functions.ts @@ -19,7 +19,7 @@ export class Functions { public async get(functionId: string): Promise { return await AppwriteCall(this.functions.get(functionId)); } - public async update(functionId: string, name: string, execute: string, vars: Vars, events: string[], schedule?: string, timeout?: number): Promise { + public async update(functionId: string, name: string, execute: string[], vars?: Vars, events?: string[], schedule?: string, timeout?: number): Promise { return await AppwriteCall(this.functions.update(functionId, name, execute, vars, events, schedule, timeout)); } public async updateTag(functionId: string, tagId: string): Promise { diff --git a/src/appwrite/Storage.ts b/src/appwrite/Storage.ts index 4ecb95d..a27dd4f 100644 --- a/src/appwrite/Storage.ts +++ b/src/appwrite/Storage.ts @@ -1,3 +1,4 @@ +import { ReadStream } from 'node:fs'; import { Client, FilesList, StorageClient } from "../appwrite"; import { AppwriteSDK } from '../constants'; import AppwriteCall from "../utils/AppwriteCall"; @@ -12,4 +13,8 @@ export class Storage { public async listFiles(): Promise { return await AppwriteCall(this.storage.listFiles()); } + + public async createFile(file: ReadStream): Promise { + return await AppwriteCall(this.storage.createFile(file)); + } } diff --git a/src/commands/common/editValue.ts b/src/commands/common/editValue.ts new file mode 100644 index 0000000..92a101f --- /dev/null +++ b/src/commands/common/editValue.ts @@ -0,0 +1,9 @@ +import { EditableTreeItem } from '../../tree/common/SimpleEditableTreeItem'; + +export async function editValue(treeItem: EditableTreeItem): Promise { + if (treeItem === undefined) { + return; + } + + await treeItem.prompt(); +} diff --git a/src/commands/functions/activateTag.ts b/src/commands/functions/activateTag.ts new file mode 100644 index 0000000..534a05a --- /dev/null +++ b/src/commands/functions/activateTag.ts @@ -0,0 +1,7 @@ +import { functionsClient } from '../../client'; +import { TagTreeItem } from '../../tree/functions/tags/TagTreeItem'; + +export async function activateTag(tagItem: TagTreeItem): Promise { + const tag = tagItem.tag; + await functionsClient?.updateTag(tag.functionId, tag.$id); +} diff --git a/src/commands/functions/copyExecutionErrors.ts b/src/commands/functions/copyExecutionErrors.ts new file mode 100644 index 0000000..889bdd0 --- /dev/null +++ b/src/commands/functions/copyExecutionErrors.ts @@ -0,0 +1,11 @@ +import { env } from 'vscode'; +import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; + +export async function copyExecutionErrors(executionItem: ExecutionTreeItem): Promise { + if (executionItem === undefined) { + return; + } + + const execution = executionItem.execution; + env.clipboard.writeText(execution.stderr); +} diff --git a/src/commands/functions/copyExecutionOutput.ts b/src/commands/functions/copyExecutionOutput.ts new file mode 100644 index 0000000..a1edf27 --- /dev/null +++ b/src/commands/functions/copyExecutionOutput.ts @@ -0,0 +1,11 @@ +import { env } from 'vscode'; +import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; + +export async function copyExecutionOutput(executionItem: ExecutionTreeItem): Promise { + if (executionItem === undefined) { + return; + } + + const execution = executionItem.execution; + env.clipboard.writeText(execution.stdout); +} diff --git a/src/commands/functions/createFunction.ts b/src/commands/functions/createFunction.ts new file mode 100644 index 0000000..c90d48c --- /dev/null +++ b/src/commands/functions/createFunction.ts @@ -0,0 +1,19 @@ +import { window } from 'vscode'; +import { functionsClient } from '../../client'; +import { appwriteFunctionRuntimes } from '../../constants'; +import { validateFunctionName } from '../../tree/functions/settings/NameTreeItem'; + +export async function createFunction(): Promise { + + const name = await window.showInputBox({ prompt: 'Function name', validateInput: validateFunctionName }); + if (name === undefined) { + return; + } + + const env: string | undefined = await window.showQuickPick(appwriteFunctionRuntimes); + if (env === undefined) { + return; + } + + await functionsClient?.create(name, [], env); +} diff --git a/src/commands/functions/createFunctionVar.ts b/src/commands/functions/createFunctionVar.ts new file mode 100644 index 0000000..d2d7c77 --- /dev/null +++ b/src/commands/functions/createFunctionVar.ts @@ -0,0 +1,16 @@ +import { functionsClient } from '../../client'; +import { VarsTreeItem } from '../../tree/functions/settings/VarsTreeItem'; +import { keyValuePrompt } from '../../tree/functions/settings/VarTreeItem'; + +export async function createFunctionVar(treeItem: VarsTreeItem): Promise { + if (treeItem === undefined) { + return; + } + const func = treeItem.parent.func; + const keyval = await keyValuePrompt(); + if (keyval) { + const newVars = {...func.vars}; + newVars[keyval.key] = keyval.value; + await functionsClient?.update(func.$id, func.name, [], newVars, func.events, func.schedule, func.timeout); + } +} diff --git a/src/commands/functions/createTag.ts b/src/commands/functions/createTag.ts index 440145b..c44888e 100644 --- a/src/commands/functions/createTag.ts +++ b/src/commands/functions/createTag.ts @@ -1,14 +1,22 @@ -import { Uri } from 'vscode'; -import { functionsClient } from '../../client'; -import { getTarReadStream } from '../../utils/tar'; -import { ext } from '../../extensionVariables'; +import { Uri } from "vscode"; +import { functionsClient, storageClient } from "../../client"; +import { getTarReadStream } from "../../utils/tar"; +import { ext } from "../../extensionVariables"; +import * as fs from "fs"; export async function createTag(folder: Uri): Promise { - const buffer = await getTarReadStream(folder); - if (buffer !== undefined) { - try { - await functionsClient?.createTag('60b1836a8e5d9', "python hello.py", buffer); - } catch (e) { - ext.outputChannel.appendLog("Creating tag error: " + e); - } + const tarFilePath = await getTarReadStream(folder); + if (functionsClient === undefined) { + return; + } + + if (tarFilePath === undefined) { + ext.outputChannel.appendLog("Error creating tar file."); + return; + } + try { + await functionsClient.createTag("60b1836a8e5d9", "python ./hello.py", fs.createReadStream(tarFilePath)); + await storageClient?.createFile(fs.createReadStream(tarFilePath)); + } catch (e) { + ext.outputChannel.appendLog("Creating tag error: " + e); } } diff --git a/src/commands/functions/deleteFunction.ts b/src/commands/functions/deleteFunction.ts new file mode 100644 index 0000000..8c48e01 --- /dev/null +++ b/src/commands/functions/deleteFunction.ts @@ -0,0 +1,9 @@ +import { functionsClient } from '../../client'; +import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem'; + +export async function deleteFunction(treeItem: FunctionTreeItem): Promise { + if (!treeItem) { + return; + } + await functionsClient?.delete(treeItem.func.$id); +} diff --git a/src/commands/functions/deleteFunctionVar.ts b/src/commands/functions/deleteFunctionVar.ts new file mode 100644 index 0000000..cac27e3 --- /dev/null +++ b/src/commands/functions/deleteFunctionVar.ts @@ -0,0 +1,13 @@ +import { functionsClient } from '../../client'; +import { VarTreeItem } from '../../tree/functions/settings/VarTreeItem'; + +export async function deleteFunctionVar(treeItem: VarTreeItem): Promise { + if (treeItem === undefined) { + return; + } + + const func = treeItem.func; + const newVars = {...func.vars}; + delete newVars[treeItem.key]; + await functionsClient?.update(func.$id, func.name, [], newVars, func.events, func.schedule, func.timeout); +} diff --git a/src/commands/functions/deleteTag.ts b/src/commands/functions/deleteTag.ts new file mode 100644 index 0000000..1d3b885 --- /dev/null +++ b/src/commands/functions/deleteTag.ts @@ -0,0 +1,11 @@ +import { functionsClient } from "../../client"; +import { TagTreeItem } from "../../tree/functions/tags/TagTreeItem"; + +export async function deleteTag(tagItem: TagTreeItem): Promise { + if (tagItem === undefined) { + return; + } + + const func = tagItem.parent.parent.func; + await functionsClient?.deleteTag(func.$id, tagItem.tag.$id); +} diff --git a/src/commands/functions/openExecutionsInBrowser.ts b/src/commands/functions/openExecutionsInBrowser.ts new file mode 100644 index 0000000..9da4338 --- /dev/null +++ b/src/commands/functions/openExecutionsInBrowser.ts @@ -0,0 +1,15 @@ +import { clientConfig } from '../../client'; +import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; +import { openUrl } from '../../utils/openUrl'; +import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; + +export async function openExecutionsInBrowser(treeItem: ExecutionsTreeItem): Promise { + + const func = treeItem.parent.func; + + const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); + // https://console.streamlux.com/console/functions/function/logs?id=60b1836a8e5d9&project=605ce39a30c01 + + const url = `${consoleUrl}/functions/function/logs?id=${func.$id}&project=${clientConfig.projectId}`; + openUrl(url); +} diff --git a/src/commands/functions/openFunctionSettingsInBrowser.ts b/src/commands/functions/openFunctionSettingsInBrowser.ts new file mode 100644 index 0000000..e90fa8d --- /dev/null +++ b/src/commands/functions/openFunctionSettingsInBrowser.ts @@ -0,0 +1,15 @@ +import { clientConfig } from '../../client'; +import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; +import { openUrl } from '../../utils/openUrl'; +import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; + +export async function openFunctionSettingsInBrowser(treeItem: ExecutionsTreeItem): Promise { + + const func = treeItem.parent.func; + + const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); + // https://console.streamlux.com/console/functions/function/settings?id=60b1836a8e5d9&project=605ce39a30c01 + + const url = `${consoleUrl}/functions/function/settings?id=${func.$id}&project=${clientConfig.projectId}`; + openUrl(url); +} diff --git a/src/commands/functions/openFunctionTagsInBrowser.ts b/src/commands/functions/openFunctionTagsInBrowser.ts new file mode 100644 index 0000000..31c40a1 --- /dev/null +++ b/src/commands/functions/openFunctionTagsInBrowser.ts @@ -0,0 +1,13 @@ +import { clientConfig } from '../../client'; +import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; +import { openUrl } from '../../utils/openUrl'; +import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; + +export async function openFunctionTagsInBrowser(treeItem: ExecutionsTreeItem): Promise { + const func = treeItem.parent.func; + + const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); + // https://console.streamlux.com/console/functions/function?id=60b1836a8e5d9&project=605ce39a30c01 + const url = `${consoleUrl}/functions/function?id=${func.$id}&project=${clientConfig.projectId}`; + openUrl(url); +} diff --git a/src/commands/functions/viewExecutionErrors.ts b/src/commands/functions/viewExecutionErrors.ts new file mode 100644 index 0000000..3d5c154 --- /dev/null +++ b/src/commands/functions/viewExecutionErrors.ts @@ -0,0 +1,11 @@ +import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; +import { openReadOnlyContent } from "../../ui/openReadonlyContent"; + +export async function viewExecutionErrors(executionItem: ExecutionTreeItem): Promise { + if (executionItem === undefined) { + return; + } + + const execution = executionItem.execution; + await openReadOnlyContent({ label: `${executionItem.parent.parent.func.name} execution stderr`, fullId: `${execution.$id}-errors.txt` }, execution.stderr, '.txt'); +} diff --git a/src/commands/functions/viewExecutionOutput.ts b/src/commands/functions/viewExecutionOutput.ts new file mode 100644 index 0000000..94e1415 --- /dev/null +++ b/src/commands/functions/viewExecutionOutput.ts @@ -0,0 +1,11 @@ +import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; +import { openReadOnlyContent } from "../../ui/openReadonlyContent"; + +export async function viewExecutionOutput(executionItem: ExecutionTreeItem): Promise { + if (executionItem === undefined) { + return; + } + + const execution = executionItem.execution; + await openReadOnlyContent({ label: `${executionItem.parent.parent.func.name} execution stdout`, fullId: `${execution.$id}-output.txt` }, execution.stdout, '.txt'); +} diff --git a/src/commands/openDocumentation.ts b/src/commands/openDocumentation.ts index 17e2367..83be656 100644 --- a/src/commands/openDocumentation.ts +++ b/src/commands/openDocumentation.ts @@ -5,7 +5,8 @@ const documentationLinks = { users: 'https://appwrite.io/docs/server/users', database: 'https://appwrite.io/docs/client/database', health: 'https://appwrite.io/docs/server/health', - storage: 'https://appwrite.io/docs/client/storage' + storage: 'https://appwrite.io/docs/client/storage', + functions: 'https://appwrite.io/docs/server/functions' }; type DocsPage = keyof typeof documentationLinks; diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 2abe6e9..5b6a27d 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -27,6 +27,20 @@ import { setActiveProject } from "./project/setActiveProject"; import { removeProject } from "./project/removeProject"; import { createTag } from './functions/createTag'; import { createExecution } from './functions/createExecution'; +import { activateTag } from './functions/activateTag'; +import { editValue } from './common/editValue'; +import { deleteFunction } from './functions/deleteFunction'; +import { createFunction } from './functions/createFunction'; +import { createFunctionVar } from './functions/createFunctionVar'; +import { deleteFunctionVar } from './functions/deleteFunctionVar'; +import { deleteTag } from './functions/deleteTag'; +import { viewExecutionErrors } from './functions/viewExecutionErrors'; +import { viewExecutionOutput } from './functions/viewExecutionOutput'; +import { copyExecutionErrors } from './functions/copyExecutionErrors'; +import { copyExecutionOutput } from './functions/copyExecutionOutput'; +import { openExecutionsInBrowser } from './functions/openExecutionsInBrowser'; +import { openFunctionSettingsInBrowser } from './functions/openFunctionSettingsInBrowser'; +import { openFunctionTagsInBrowser } from './functions/openFunctionTagsInBrowser'; class CommandRegistrar { constructor(private readonly context: ExtensionContext) {} @@ -58,6 +72,9 @@ export function registerCommands(context: ExtensionContext): void { }); }; + /** Common **/ + registerCommand("editValue", editValue); + /** General **/ registerCommand("Connect", connectAppwrite, "all"); @@ -102,6 +119,21 @@ export function registerCommands(context: ExtensionContext): void { registerCommand("removeProject", removeProject, "all"); /** Functions **/ - registerCommand("CreateTag", createTag, "functions"); + registerCommand("refreshFunctions", undefined, "functions"); registerCommand("CreateExecution", createExecution, "functions"); + registerCommand("CreateTag", createTag, "functions"); + registerCommand("activateTag", activateTag, "functions"); + registerCommand("deleteTag", deleteTag, "functions"); + registerCommand("deleteFunction", deleteFunction, "functions"); + registerCommand("openFunctionsDocumentation", () => openDocumentation("functions")); + registerCommand("createFunction", createFunction, "functions"); + registerCommand("createFunctionVar", createFunctionVar, "functions"); + registerCommand("deleteFunctionVar", deleteFunctionVar, "functions"); + registerCommand("viewExecutionErrors", viewExecutionErrors); + registerCommand("viewExecutionOutput", viewExecutionOutput); + registerCommand("copyExecutionOutput", copyExecutionOutput); + registerCommand("copyExecutionErrors", copyExecutionErrors); + registerCommand("openExecutionsInBrowser", openExecutionsInBrowser); + registerCommand("openFunctionTagsInBrowser", openFunctionTagsInBrowser); + registerCommand("openFunctionSettingsInBrowser", openFunctionSettingsInBrowser); } diff --git a/src/commands/users/openUserInConsole.ts b/src/commands/users/openUserInConsole.ts index 7f2df30..9853eff 100644 --- a/src/commands/users/openUserInConsole.ts +++ b/src/commands/users/openUserInConsole.ts @@ -3,7 +3,7 @@ import { commands, Uri } from "vscode"; import { clientConfig } from "../../client"; import { UserTreeItem } from "../../tree/users/UserTreeItem"; -function getConsoleUrlFromEndpoint(endpoint: string): string { +export function getConsoleUrlFromEndpoint(endpoint: string): string { const url = new URL(endpoint); return `${url.origin}/console`; } diff --git a/src/constants.ts b/src/constants.ts index 34e9127..40276cc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,197 @@ -import type { SDK } from './appwrite'; +import type { SDK } from "./appwrite"; // eslint-disable-next-line @typescript-eslint/no-var-requires -export const AppwriteSDK: SDK = require('node-appwrite') as SDK; +export const AppwriteSDK: SDK = require("node-appwrite") as SDK; + +export const appwriteSystemEvents = [ + { + name: "account.create", + description: "This event triggers when the account is created.", + }, + { + name: "account.update.email", + description: "This event triggers when the account email address is updated.", + }, + { + name: "account.update.name", + description: "This event triggers when the account name is updated.", + }, + { + name: "account.update.password", + description: "This event triggers when the account password is updated.", + }, + { + name: "account.update.prefs", + description: "This event triggers when the account preferences are updated.", + }, + { + name: "account.recovery.create", + description: "This event triggers when the account recovery token is created.", + }, + { + name: "account.recovery.update", + description: "This event triggers when the account recovery token is validated.", + }, + { + name: "account.verification.create", + description: "This event triggers when the account verification token is created.", + }, + { + name: "account.verification.update", + description: "This event triggers when the account verification token is validated.", + }, + { + name: "account.delete", + description: "This event triggers when the account is deleted.", + }, + { + name: "account.sessions.create", + description: "This event triggers when the account session is created.", + }, + { + name: "account.delete", + description: "This event triggers when the account is deleted.", + }, + { + name: "account.sessions.create", + description: "This event triggers when the account session is created.", + }, + { + name: "account.sessions.delete", + description: "This event triggers when the account session is deleted.", + }, + { + name: "database.collections.create", + description: "This event triggers when a database collection is created.", + }, + { + name: "database.collections.update", + description: "This event triggers when a database collection is updated.", + }, + { + name: "database.collections.delete", + description: "This event triggers when a database collection is deleted.", + }, + { + name: "database.documents.create", + description: "This event triggers when a database document is created.", + }, + { + name: "database.documents.update", + description: "This event triggers when a database document is updated.", + }, + { + name: "database.documents.delete", + description: "This event triggers when a database document is deleted.", + }, + { + name: "functions.create", + description: "This event triggers when a function is created.", + }, + { + name: "functions.update", + description: "This event triggers when a function is updated.", + }, + { + name: "functions.delete", + description: "This event triggers when a function is deleted.", + }, + { + name: "functions.tags.create", + description: "This event triggers when a function tag is created.", + }, + { + name: "functions.tags.update", + description: "This event triggers when a function tag is updated.", + }, + { + name: "functions.tags.delete", + description: "This event triggers when a function tag is deleted.", + }, + { + name: "functions.executions.create", + description: "This event triggers when a function execution is created.", + }, + { + name: "functions.executions.update", + description: "This event triggers when a function execution is updated.", + }, + { + name: "storage.files.create", + description: "This event triggers when a storage file is created.", + }, + { + name: "storage.files.update", + description: "This event triggers when a storage file is updated.", + }, + { + name: "storage.files.delete", + description: "This event triggers when a storage file is deleted.", + }, + { + name: "users.create", + description: "This event triggers when a user is created from the users API.", + }, + { + name: "users.update.prefs", + description: "This event triggers when a user preference is updated from the users API.", + }, + { + name: "users.update.status", + description: "This event triggers when a user status is updated from the users API.", + }, + { + name: "users.delete", + description: "This event triggers when a user is deleted from users API.", + }, + { + name: "users.sessions.delete", + description: "This event triggers when a user session is deleted from users API.", + }, + { + name: "teams.create", + description: "This event triggers when a team is created.", + }, + { + name: "teams.update", + description: "This event triggers when a team is updated.", + }, + { + name: "teams.delete", + description: "This event triggers when a team is deleted.", + }, + { + name: "teams.memberships.create", + description: "This event triggers when a team memberships is created.", + }, + { + name: "teams.memberships.update", + description: "This event triggers when a team membership is updated.", + }, + { + name: "teams.memberships.update.status", + description: "This event triggers when a team memberships status is updated.", + }, + { + name: "teams.memberships.delete", + description: "This event triggers when a team memberships is deleted.", + }, +]; + +export const appwriteFunctionRuntimes = [ + "dotnet-3.1", + "dotnet-5.0", + "dart-2.10", + "dart-2.12", + "deno-1.5", + "deno-1.6", + "deno-1.8", + "python-3.8", + "python-3.9", + "ruby-2.7", + "ruby-3.0", + "php-7.4", + "php-8.0", + "node-14.5", + "node-15.5", +]; diff --git a/src/tree/common/EditableTreeItemBase.ts b/src/tree/common/EditableTreeItemBase.ts new file mode 100644 index 0000000..70bfd00 --- /dev/null +++ b/src/tree/common/EditableTreeItemBase.ts @@ -0,0 +1,13 @@ +import { TreeItem } from "vscode"; + +export abstract class EditableTreeItemBase extends TreeItem { + public abstract setValue(value: T): Promise; + + constructor(contextValuePrefix: string, public readonly value: T, description?: string) { + super(typeof value === "string" ? value : "No label"); + this.contextValue = `editable_${contextValuePrefix}`; + this.description = description ?? contextValuePrefix; + } + + public abstract prompt(): Promise; +} diff --git a/src/tree/common/EnumEditableTreeItem.ts b/src/tree/common/EnumEditableTreeItem.ts new file mode 100644 index 0000000..9969f9b --- /dev/null +++ b/src/tree/common/EnumEditableTreeItem.ts @@ -0,0 +1,38 @@ +import { QuickPickItem, QuickPickOptions, window } from "vscode"; +import { EditableTreeItemBase } from "./EditableTreeItemBase"; + +export abstract class EnumEditableTreeItemBase extends EditableTreeItemBase { + public abstract options: string[] | QuickPickItem[]; + + public quickPickOptions: QuickPickOptions; + + constructor(contextValuePrefix: string, public readonly value: string[], description?: string) { + super(contextValuePrefix, value, description); + this.quickPickOptions = {}; + } + + public async prompt(): Promise { + + const value = await window.showQuickPick( + this.options.map((option: QuickPickItem | string): QuickPickItem => { + if (typeof option === "string") { + return { label: option, picked: this.value.includes(option) }; + } + const picked = this.value.includes(option.label); + return { ...option, picked, alwaysShow: picked }; + }).sort((a, b) => { + if (a.picked) { + return -1; + } + if (b.picked) { + return 1; + } + return 0; + }), + { ...this.quickPickOptions, canPickMany: true } + ); + if (value !== undefined) { + this.setValue(value.map((item) => item.label)); + } + } +} diff --git a/src/tree/common/SimpleEditableTreeItem.ts b/src/tree/common/SimpleEditableTreeItem.ts new file mode 100644 index 0000000..834e299 --- /dev/null +++ b/src/tree/common/SimpleEditableTreeItem.ts @@ -0,0 +1,18 @@ +import { TreeItem, window } from "vscode"; + +export class EditableTreeItem extends TreeItem { + public readonly setValue: (value: string) => Promise; + + constructor(label: string, contextValuePrefix: string, public readonly value: string, setValue: (value: string) => Promise) { + super(label); + this.setValue = setValue; + this.contextValue = `editable_${contextValuePrefix}`; + } + + public async prompt(): Promise { + const value = await window.showInputBox({ value: this.value }); + if (value !== undefined) { + this.setValue(value); + } + } +} diff --git a/src/tree/common/StringEditableTreeItem.ts b/src/tree/common/StringEditableTreeItem.ts new file mode 100644 index 0000000..e4f77b0 --- /dev/null +++ b/src/tree/common/StringEditableTreeItem.ts @@ -0,0 +1,22 @@ +import { InputBoxOptions, window } from "vscode"; +import { EditableTreeItemBase } from "./EditableTreeItemBase"; + +export abstract class StringEditableTreeItemBase extends EditableTreeItemBase { + public abstract setValue(value: string): Promise; + public inputBoxOptions: InputBoxOptions; + + constructor(contextValuePrefix: string, public readonly value: string, description?: string) { + super(contextValuePrefix, value, description); + + this.inputBoxOptions = { + prompt: description, + }; + } + + public async prompt(): Promise { + const value = await window.showInputBox({ value: this.value, ...this.inputBoxOptions }); + if (value !== undefined) { + this.setValue(value); + } + } +} diff --git a/src/tree/functions/FunctionTreeItem.ts b/src/tree/functions/FunctionTreeItem.ts index ba116a2..ae5e1ed 100644 --- a/src/tree/functions/FunctionTreeItem.ts +++ b/src/tree/functions/FunctionTreeItem.ts @@ -1,17 +1,21 @@ -import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; import { Function } from "../../appwrite"; import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; -import { ExecutionsTreeItem } from './ExecutionsTreeItem'; +import { msToDate } from '../../utils/date'; +import { ExecutionsTreeItem } from './executions/ExecutionsTreeItem'; import { FunctionsTreeItemProvider } from './FunctionsTreeItemProvider'; -import { TagsTreeItem } from './TagsTreeItem'; +import { FunctionSettingsTreeItem } from './settings/FunctionSettingsTreeItem'; +import { TagsTreeItem } from './tags/TagsTreeItem'; export class FunctionTreeItem extends AppwriteTreeItemBase { constructor(public func: Function, public readonly provider: FunctionsTreeItemProvider) { super(undefined, func.name); + this.tooltip = new MarkdownString(`ID: ${func.$id} \nLast updated: ${msToDate(func.dateUpdated)} \nCreated: ${msToDate(func.dateCreated)}`); + this.description = func.env; } public async getChildren(): Promise { - return [new TagsTreeItem(this), new ExecutionsTreeItem(this)]; + return [new FunctionSettingsTreeItem(this), new TagsTreeItem(this), new ExecutionsTreeItem(this)]; } public async refresh(): Promise { diff --git a/src/tree/functions/FunctionsTreeItemProvider.ts b/src/tree/functions/FunctionsTreeItemProvider.ts index 78fe199..8b9b719 100644 --- a/src/tree/functions/FunctionsTreeItemProvider.ts +++ b/src/tree/functions/FunctionsTreeItemProvider.ts @@ -37,10 +37,7 @@ export class FunctionsTreeItemProvider implements vscode.TreeDataProvider new FunctionTreeItem(func, this)) ?? []; - const headerItem: vscode.TreeItem = { - label: `Total functions: ${list.sum}`, - }; - return [headerItem, ...functionTreeItems]; + return functionTreeItems; } return [{ label: "No functions found" }]; diff --git a/src/tree/functions/ExecutionTreeItem.ts b/src/tree/functions/executions/ExecutionTreeItem.ts similarity index 67% rename from src/tree/functions/ExecutionTreeItem.ts rename to src/tree/functions/executions/ExecutionTreeItem.ts index 00aaef5..6fe773f 100644 --- a/src/tree/functions/ExecutionTreeItem.ts +++ b/src/tree/functions/executions/ExecutionTreeItem.ts @@ -1,8 +1,8 @@ -import dayjs = require('dayjs'); import { MarkdownString, ThemeColor, ThemeIcon, TreeItem } from "vscode"; -import { Execution, ExecutionStatus } from "../../appwrite"; -import { functionsClient } from "../../client"; -import { ext } from "../../extensionVariables"; +import { Execution, ExecutionStatus } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { ext } from "../../../extensionVariables"; +import { msToDate } from "../../../utils/date"; import { ExecutionsTreeItem } from "./ExecutionsTreeItem"; const executionStatusIcons: Record = { @@ -22,13 +22,14 @@ export class ExecutionTreeItem extends TreeItem { public isAutoRefreshing: boolean = false; private refreshCount: number = 0; - constructor(public readonly parent: ExecutionsTreeItem, private readonly execution: Execution) { + constructor(public readonly parent: ExecutionsTreeItem, public readonly execution: Execution) { super(execution.$id); this.label = this.getLabel(execution); this.iconPath = executionStatusIcons[execution.status]; const md = `Id: ${execution.$id} \nCreated: ${this.getCreated(execution)} \nTrigger: ${execution.trigger}`; this.tooltip = new MarkdownString(md); this.description = execution.trigger; + this.contextValue = this.getContextValue(execution); this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting"; this.autoRefresh(); } @@ -47,6 +48,7 @@ export class ExecutionTreeItem extends TreeItem { return; } + this.contextValue = this.getContextValue(execution); this.iconPath = executionStatusIcons[execution.status]; this.label = this.getLabel(execution); this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting"; @@ -57,17 +59,28 @@ export class ExecutionTreeItem extends TreeItem { } getLabel(execution: Execution): string { - if (execution.status === "completed") { + if (execution.status === "completed" || execution.status === "failed") { return `${this.getCreated(execution)} (${execution.time.toPrecision(2)}s)`; } return `${this.getCreated(execution)} (${execution.status})`; } - getCreated(execution: Execution): string { - return dayjs(execution.dateCreated).format("LTS"); + getContextValue(execution: Execution): string { + if (execution.status === "completed" || execution.status === "failed") { + if (execution.stderr === "" && execution.stdout === "") { + return "execution_noErrorOrOutput"; + } + if (execution.stderr === "") { + return "execution_outputOnly"; + } + if (execution.stdout === "") { + return "execution_errorOnly"; + } + } + return "execution"; } - - - contextValue = "tag"; + getCreated(execution: Execution): string { + return msToDate(execution.dateCreated); + } } diff --git a/src/tree/functions/ExecutionsTreeItem.ts b/src/tree/functions/executions/ExecutionsTreeItem.ts similarity index 79% rename from src/tree/functions/ExecutionsTreeItem.ts rename to src/tree/functions/executions/ExecutionsTreeItem.ts index 12994f0..916d2e1 100644 --- a/src/tree/functions/ExecutionsTreeItem.ts +++ b/src/tree/functions/executions/ExecutionsTreeItem.ts @@ -1,9 +1,9 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; -import { Execution, ExecutionList } from '../../appwrite'; -import { functionsClient } from "../../client"; -import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; +import { Execution, ExecutionList } from '../../../appwrite'; +import { functionsClient } from "../../../client"; +import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; import { ExecutionTreeItem } from './ExecutionTreeItem'; -import { FunctionTreeItem } from './FunctionTreeItem'; +import { FunctionTreeItem } from '../FunctionTreeItem'; export class ExecutionsTreeItem extends AppwriteTreeItemBase { constructor(public readonly parent: FunctionTreeItem) { diff --git a/src/tree/functions/settings/EventsTreeItem.ts b/src/tree/functions/settings/EventsTreeItem.ts new file mode 100644 index 0000000..2346b13 --- /dev/null +++ b/src/tree/functions/settings/EventsTreeItem.ts @@ -0,0 +1,31 @@ +import { QuickPickItem, QuickPickOptions } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { appwriteSystemEvents } from "../../../constants"; +import { ext } from "../../../extensionVariables"; +import { EnumEditableTreeItemBase } from "../../common/EnumEditableTreeItem"; +import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; + +export class EventsTreeItem extends EnumEditableTreeItemBase { + public quickPickOptions: QuickPickOptions = { + placeHolder: "Select which system events should trigger this function.", + matchOnDescription: true + }; + public options: string[] | QuickPickItem[] = appwriteSystemEvents.map((event) => ({ + label: event.name, + description: event.description.replace("This event t", "T") + })); + + public readonly func: Function; + + constructor(public readonly parent: FunctionSettingsTreeItem) { + super("System events", parent.func.events); + this.func = parent.func; + this.label = parent.func.events.length === 0 ? 'None' : `${parent.func.events.length} active`; + } + + public async setValue(value: string[]): Promise { + await functionsClient?.update(this.func.$id, this.func.name, [], this.func.vars, value, this.func.schedule, this.func.timeout); + ext.tree?.functions?.refresh(); + } +} diff --git a/src/tree/functions/settings/FunctionSettingsTreeItem.ts b/src/tree/functions/settings/FunctionSettingsTreeItem.ts new file mode 100644 index 0000000..941c2a9 --- /dev/null +++ b/src/tree/functions/settings/FunctionSettingsTreeItem.ts @@ -0,0 +1,45 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; +import { ChildTreeItem } from "../../ChildTreeItem"; +import { FunctionTreeItem } from "../FunctionTreeItem"; +import { EventsTreeItem } from "./EventsTreeItem"; +import { NameTreeItem } from "./NameTreeItem"; +import { ScheduleTreeItem } from "./ScheduleTreeItem"; +import { TimeoutTreeItem } from "./TimeoutTreeItem"; +import { VarsTreeItem } from "./VarsTreeItem"; + +export class FunctionSettingsTreeItem extends AppwriteTreeItemBase { + public readonly func: Function; + + constructor(public readonly parent: FunctionTreeItem) { + super(parent, "Settings"); + this.func = parent.func; + } + + public async getChildren(): Promise { + if (!functionsClient) { + return []; + } + + const children = [ + new NameTreeItem(this), + new ScheduleTreeItem(this), + new TimeoutTreeItem(this.func), + new EventsTreeItem(this), + new VarsTreeItem(this), + ]; + return children; + } + + labelItem(label: string, value: string): TreeItem { + return new ChildTreeItem(this, { label: value === "" ? "None" : value, description: label }); + } + + collapsibleState = TreeItemCollapsibleState.Collapsed; + + contextValue = "functionSettings"; + + iconPath = new ThemeIcon("settings"); +} diff --git a/src/tree/functions/settings/NameTreeItem.ts b/src/tree/functions/settings/NameTreeItem.ts new file mode 100644 index 0000000..159fd0f --- /dev/null +++ b/src/tree/functions/settings/NameTreeItem.ts @@ -0,0 +1,43 @@ +import { InputBoxOptions, MarkdownString } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { ext } from "../../../extensionVariables"; +import { StringEditableTreeItemBase } from '../../common/StringEditableTreeItem'; +import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; + +const tooltip = "Function name"; +const description = "Function name. Max length: 128 chars."; +const tooLongInvalid = "Value exceeds maximum length of 128 characters."; + +export function validateFunctionName(value: string): string | undefined { + if (value.length > 128) { + return tooLongInvalid; + } +} + +export class NameTreeItem extends StringEditableTreeItemBase { + public readonly func: Function; + + inputBoxOptions: InputBoxOptions = { + validateInput: (value) => { + if (value.length > 128) { + return tooLongInvalid; + } + }, + prompt: description, + }; + + public async setValue(value: string): Promise { + if (value.length === 0) { + return; + } + await functionsClient?.update(this.func.$id, value, [], this.func.vars, this.func.events, this.func.schedule, this.func.timeout); + ext.tree?.functions?.refresh(); + } + + constructor(private readonly parent: FunctionSettingsTreeItem) { + super("Name", parent.func.name); + this.func = parent.func; + this.tooltip = new MarkdownString(tooltip); + } +} diff --git a/src/tree/functions/settings/ScheduleTreeItem.ts b/src/tree/functions/settings/ScheduleTreeItem.ts new file mode 100644 index 0000000..de2e3bb --- /dev/null +++ b/src/tree/functions/settings/ScheduleTreeItem.ts @@ -0,0 +1,44 @@ +import { InputBoxOptions, MarkdownString } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { ext } from "../../../extensionVariables"; +import cron from "cron-validate"; +import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; +import cronstrue from "cronstrue"; +import { StringEditableTreeItemBase } from '../../common/StringEditableTreeItem'; + +export class ScheduleTreeItem extends StringEditableTreeItemBase { + private readonly func: Function; + + inputBoxOptions: InputBoxOptions = { + validateInput: (value) => { + if (value === "") { + return; + } + const cronResult = cron(value); + if (!cronResult.isValid()) { + return cronResult.getError().join(", "); + } + }, + value: this.value === "" ? "0 0 * * *" : this.value, + prompt: "Function execution schedule in CRON format. Leave blank for no schedule. https://crontab.guru/examples.html", + }; + + public async setValue(value: string): Promise { + await functionsClient?.update(this.func.$id, this.func.name, [], this.func.vars, this.func.events, value === "" ? undefined : value, this.func.timeout); + ext.tree?.functions?.refresh(); + } + + constructor(private readonly parent: FunctionSettingsTreeItem) { + super("Schedule", parent.func.schedule); + this.func = parent.func; + this.tooltip = new MarkdownString(`Function execution schedule in CRON format`); + this.label = `${this.value}`; + const cronResult = cron(parent.func.schedule); + if (cronResult.isValid()) { + this.label = cronstrue.toString(this.value, { verbose: true }); + } else { + this.label = this.value === "" ? "None" : "Invalid CRON"; + } + } +} diff --git a/src/tree/functions/settings/TimeoutTreeItem.ts b/src/tree/functions/settings/TimeoutTreeItem.ts new file mode 100644 index 0000000..3b5da4c --- /dev/null +++ b/src/tree/functions/settings/TimeoutTreeItem.ts @@ -0,0 +1,48 @@ +import { InputBoxOptions, MarkdownString } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { ext } from "../../../extensionVariables"; +import { StringEditableTreeItemBase } from "../../common/StringEditableTreeItem"; + +function isNumeric(str: string) { + console.log("here"); + return !isNaN(+str); +} + +export class TimeoutTreeItem extends StringEditableTreeItemBase { + inputBoxOptions: InputBoxOptions = { + validateInput: (value) => { + if (!isNumeric(value)) { + return "Input must be an integer."; + } + + if (+value > 900) { + return "Value exceeds the maximum of 900 seconds (15 minutes)"; + } + + if (+value < 0) { + return "Value cannot be negative"; + } + }, + prompt: "Function maximum execution time in seconds. Maximum of 900 seconds (15 minutes).", + }; + + public async setValue(value: string): Promise { + await functionsClient?.update( + this.func.$id, + this.func.name, + [], + this.func.vars, + this.func.events, + this.func.schedule, + parseInt(value) + ); + ext.tree?.functions?.refresh(); + } + + constructor(private readonly func: Function) { + super("Timeout", func.timeout.toString()); + this.tooltip = new MarkdownString(`Function maximum execution time in seconds.`); + this.label = `${this.value}s`; + } +} diff --git a/src/tree/functions/settings/VarTreeItem.ts b/src/tree/functions/settings/VarTreeItem.ts new file mode 100644 index 0000000..c1134b8 --- /dev/null +++ b/src/tree/functions/settings/VarTreeItem.ts @@ -0,0 +1,64 @@ +import { InputBoxOptions, MarkdownString, window } from "vscode"; +import { Function } from "../../../appwrite"; +import { functionsClient } from "../../../client"; +import { ext } from "../../../extensionVariables"; +import { StringEditableTreeItemBase } from "../../common/StringEditableTreeItem"; +import { VarsTreeItem } from "./VarsTreeItem"; + +const tooltip = "Environment var"; +const description = "Function name. Max length: 128 chars."; +const tooLongInvalid = "Value exceeds maximum length of 128 characters."; + +export async function keyValuePrompt(keyInit?: string, valueInit?: string): Promise<{ key: string; value: string } | undefined> { + const key = await window.showInputBox({ value: keyInit, prompt: "Environment variable name" }); + if (key === undefined) { + return; + } + const value = await window.showInputBox({ value: valueInit, prompt: "Environment variable value" }); + if (value === undefined) { + return; + } + return { key, value }; +} + +export class VarTreeItem extends StringEditableTreeItemBase { + public readonly func: Function; + + inputBoxOptions: InputBoxOptions = { + validateInput: (value) => { + if (value.length > 128) { + return tooLongInvalid; + } + }, + prompt: description, + }; + + public async setValue(value: string, key?: string): Promise { + if (value.length === 0) { + return; + } + const newVars = { ...this.func.vars }; + newVars[this.key] = value; + if (key) { + delete newVars[this.key]; + newVars[key] = value; + } + await functionsClient?.update(this.func.$id, this.func.name, [], newVars, this.func.events, this.func.schedule, this.func.timeout); + ext.tree?.functions?.refresh(); + } + + constructor(public readonly parent: VarsTreeItem, public readonly key: string, value: string) { + super("var", value); + this.func = parent.parent.func; + this.tooltip = new MarkdownString(tooltip); + this.label = `${key}=${value}`; + this.description = undefined; + } + + public async prompt(): Promise { + const keyval = await keyValuePrompt(this.key, this.value); + if (keyval) { + this.setValue(keyval.value, keyval.key); + } + } +} diff --git a/src/tree/functions/settings/VarsTreeItem.ts b/src/tree/functions/settings/VarsTreeItem.ts new file mode 100644 index 0000000..e66496b --- /dev/null +++ b/src/tree/functions/settings/VarsTreeItem.ts @@ -0,0 +1,21 @@ +import { TreeItem, TreeItemCollapsibleState } from "vscode"; +import { Vars } from "../../../appwrite"; +import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; +import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; +import { VarTreeItem } from "./VarTreeItem"; + +export class VarsTreeItem extends AppwriteTreeItemBase { + public readonly vars: Vars; + + constructor(parent: FunctionSettingsTreeItem) { + super(parent, "Environment variables"); + this.vars = parent.func.vars; + this.description = undefined; + } + + public async getChildren(): Promise { + return Object.keys(this.vars).map((key) => new VarTreeItem(this, key, this.vars[key])); + } + contextValue = "vars"; + collapsibleState = TreeItemCollapsibleState.Collapsed; +} diff --git a/src/tree/functions/TagTreeItem.ts b/src/tree/functions/tags/TagTreeItem.ts similarity index 68% rename from src/tree/functions/TagTreeItem.ts rename to src/tree/functions/tags/TagTreeItem.ts index 24f731d..5e4c821 100644 --- a/src/tree/functions/TagTreeItem.ts +++ b/src/tree/functions/tags/TagTreeItem.ts @@ -1,16 +1,14 @@ import { ThemeIcon, TreeItem } from "vscode"; -import { Tag } from '../../appwrite'; +import { Tag } from '../../../appwrite'; import { TagsTreeItem } from './TagsTreeItem'; export class TagTreeItem extends TreeItem { - - constructor(public readonly parent: TagsTreeItem, tag: Tag) { + constructor(public readonly parent: TagsTreeItem, public readonly tag: Tag) { super(tag.$id); const func = parent.parent.func; const active = func.tag === tag.$id; this.label = `${tag.$id}${active ? ' (Active)' : ''}`; this.iconPath = new ThemeIcon(active ? 'circle-filled' : 'circle-outline'); + this.contextValue = `tag${active ? '_active' : ''}`; } - - contextValue = "tag"; } diff --git a/src/tree/functions/TagsTreeItem.ts b/src/tree/functions/tags/TagsTreeItem.ts similarity index 80% rename from src/tree/functions/TagsTreeItem.ts rename to src/tree/functions/tags/TagsTreeItem.ts index 63d5532..3f65829 100644 --- a/src/tree/functions/TagsTreeItem.ts +++ b/src/tree/functions/tags/TagsTreeItem.ts @@ -1,7 +1,7 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; -import { functionsClient } from "../../client"; -import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; -import { FunctionTreeItem } from './FunctionTreeItem'; +import { functionsClient } from "../../../client"; +import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; +import { FunctionTreeItem } from '../FunctionTreeItem'; import { TagTreeItem } from './TagTreeItem'; export class TagsTreeItem extends AppwriteTreeItemBase { diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..f5da0cc --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,5 @@ +import dayjs = require('dayjs'); + +export function msToDate(ms: number): string { + return dayjs(ms).format("LTS"); +} diff --git a/src/utils/tar.ts b/src/utils/tar.ts index 18c1fc5..8f81821 100644 --- a/src/utils/tar.ts +++ b/src/utils/tar.ts @@ -4,14 +4,13 @@ import { ext } from "../extensionVariables"; import * as path from "path"; import * as os from "os"; import * as fs from "fs"; -import { ReadStream } from 'node:fs'; -export async function getTarReadStream(folder: Uri): Promise { +export async function getTarReadStream(folder: Uri): Promise { try { const folderName = path.basename(folder.path); const tarName = `${folderName}.tar.gz`; - const cwd = folder.fsPath; + const cwd = path.resolve(folder.fsPath, '..'); if (cwd === undefined) { window.showErrorMessage("No workspace open."); return; @@ -20,17 +19,11 @@ export async function getTarReadStream(folder: Uri): Promise { - try { - fs.unlinkSync(tarFilePath); - } catch (e) { - // - } - }); - return stream; + ext.outputChannel.appendLog(`Created ${tarFilePath}`); + + return tarFilePath; } catch (e) { ext.outputChannel?.appendLog("Error creating tar.gz: " + e); diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..e69de29