Feature: functions (#15)

* Basic functions create tag and create execution feature

* Detailed function support, create tag still broken

* create tag and pick folder

* edit changelog and bump extension version

* fix linting
This commit is contained in:
Alex Weininger 2021-05-29 10:32:38 -05:00 committed by GitHub
parent b4e5fdcd20
commit 6cbf15379c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1722 additions and 4573 deletions

View file

@ -6,6 +6,16 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased] ## [Unreleased]
## [0.1.0] - 2021-5-29
## Functions!
### Added
- Ability to create and delete Appwrite functions
- Edit function settings
- View, and delete tags (creating tags is broken currently)
- Create and view function executions
- View execution output and errors
## [0.0.9] - 2021-5-21 ## [0.0.9] - 2021-5-21
- Remove temporary fix for Appwrite https://github.com/appwrite/appwrite/issues/1171. Upstream issue was resolved. - Remove temporary fix for Appwrite https://github.com/appwrite/appwrite/issues/1171. Upstream issue was resolved.

4716
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "vscode-appwrite", "name": "vscode-appwrite",
"displayName": "Appwrite", "displayName": "Appwrite",
"description": "Manage your Appwrite resources right from VS Code!", "description": "Manage your Appwrite resources right from VS Code!",
"version": "0.0.9", "version": "0.1.0",
"engines": { "engines": {
"vscode": "^1.55.0" "vscode": "^1.55.0"
}, },
@ -30,7 +30,9 @@
"onView:Users", "onView:Users",
"onView:Database", "onView:Database",
"onView:Health", "onView:Health",
"onCommand:vscode-appwrite.AddProject" "onView:Functions",
"onCommand:vscode-appwrite.AddProject",
"onCommand:vscode-appwrite.CreateTag"
], ],
"main": "./dist/extension.js", "main": "./dist/extension.js",
"contributes": { "contributes": {
@ -184,10 +186,99 @@
"title": "Refresh projects", "title": "Refresh projects",
"icon": "$(refresh)" "icon": "$(refresh)"
}, },
{
"command": "vscode-appwrite.refreshFunctions",
"title": "Refresh functions",
"icon": "$(refresh)"
},
{ {
"command": "vscode-appwrite.removeProject", "command": "vscode-appwrite.removeProject",
"title": "Remove project", "title": "Remove project",
"icon": "$(trash)" "icon": "$(trash)"
},
{
"command": "vscode-appwrite.CreateTag",
"title": "Create function tag",
"icon": "$(cloud-upload)"
},
{
"command": "vscode-appwrite.deleteTag",
"title": "Delete tag",
"icon": "$(trash)"
},
{
"command": "vscode-appwrite.CreateExecution",
"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": { "views": {
@ -211,6 +302,10 @@
{ {
"id": "Projects", "id": "Projects",
"name": "Projects" "name": "Projects"
},
{
"id": "Functions",
"name": "Functions (Preview)"
} }
] ]
}, },
@ -276,14 +371,24 @@
"when": "view == Storage", "when": "view == Storage",
"group": "navigation" "group": "navigation"
}, },
{
"command": "vscode-appwrite.openFunctionsDocumentation",
"when": "view == Functions",
"group": "navigation"
},
{ {
"command": "vscode-appwrite.refreshProjects", "command": "vscode-appwrite.refreshProjects",
"when": "view == Projects", "when": "view == Projects",
"group": "navigation" "group": "navigation"
}, },
{ {
"command": "vscode-appwrite.addProject", "command": "vscode-appwrite.refreshFunctions",
"when": "view == Projects", "when": "view == Functions",
"group": "navigation"
},
{
"command": "vscode-appwrite.createFunction",
"when": "view == Functions",
"group": "navigation" "group": "navigation"
} }
], ],
@ -383,6 +488,85 @@
{ {
"command": "vscode-appwrite.removeProject", "command": "vscode-appwrite.removeProject",
"when": "viewItem =~ /(appwriteProject)/" "when": "viewItem =~ /(appwriteProject)/"
},
{
"command": "vscode-appwrite.CreateExecution",
"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"
},
{
"command": "vscode-appwrite.CreateTag",
"when": "viewItem =~ /^tags$/",
"group": "inline"
}
],
"explorer/context": [
{
"command": "vscode-appwrite.CreateTag",
"when": "explorerResourceIsFolder == true",
"group": "appwrite@1"
} }
], ],
"commandPalette": [ "commandPalette": [
@ -499,6 +683,7 @@
"@types/glob": "^7.1.3", "@types/glob": "^7.1.3",
"@types/mocha": "^8.0.4", "@types/mocha": "^8.0.4",
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"@types/tar": "^4.0.4",
"@types/vscode": "^1.55.0", "@types/vscode": "^1.55.0",
"@typescript-eslint/eslint-plugin": "^4.14.1", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1", "@typescript-eslint/parser": "^4.14.1",
@ -513,8 +698,11 @@
"webpack-cli": "^4.4.0" "webpack-cli": "^4.4.0"
}, },
"dependencies": { "dependencies": {
"cron-validate": "^1.4.3",
"cronstrue": "^1.113.0",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"node-appwrite": "^2.2.1" "node-appwrite": "^2.2.3",
"tar": "^6.1.0"
} }
} }

87
src/appwrite.d.ts vendored
View file

@ -1,3 +1,6 @@
import { ReadStream } from 'fs';
import { Stream } from 'node:stream';
export type Token = { export type Token = {
/** /**
* Token ID. * Token ID.
@ -287,7 +290,7 @@ export type Rule = {
list: string[]; list: string[];
}; };
interface Permissions { export type Permissions = {
read: string[]; read: string[];
write: string[]; write: string[];
} }
@ -356,11 +359,90 @@ export type AppwriteHealth = {
}; };
export type StorageClient = { export type StorageClient = {
createFile: (file: any, read: string[], write: string[]) => Promise<any>; createFile: (file: any, read?: string[], write?: string[]) => Promise<any>;
listFiles: () => Promise<any>; listFiles: () => Promise<any>;
getFile: (fileId: string) => Promise<any>; getFile: (fileId: string) => Promise<any>;
}; };
type Vars = Record<string, any>;
export type Function = {
'$id': string;
'$permissions': Permissions;
name: string;
dateCreated: number;
dateUpdated: number;
status: string;
env: string;
tag: string;
vars: Vars;
events: string[];
schedule: string;
scheduleNext: number;
schedulePrevious: number;
timeout: number;
}
export type FunctionsList = {
sum: number;
functions: Function[];
}
export type Tag = {
'$id': string;
functionId: string;
dateCreated: number;
command: string;
size: string;
};
export type TagList = {
sum: number;
tags: Tag[];
}
export type ExecutionStatus = "waiting" | "processing" | "completed" | "failed";
export type Execution = {
'$id': string;
functionId: string;
dateCreated: number;
trigger: string;
status: ExecutionStatus;
exitCode: number;
stdout: string;
stderr: string;
time: number;
};
export type ExecutionList = {
sum: number;
executions: Execution[];
};
export type Search = {
search?: string;
limit?: number;
offset?: number;
orderType?: 'ASC' | 'DESC';
};
export type FunctionsClient = {
create: (name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise<any>;
list: (search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC') => Promise<any>;
get: (functionId: string) => Promise<any>;
update: (functionId: string, name: string, execute: string[], vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise<any>;
updateTag: (functionId: string, tagId: string) => Promise<any>;
delete: (functionId: string) => Promise<any>;
createTag: (id: string, command: string, code: ReadStream) => Promise<any>;
listTags: (id: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC') => Promise<any>;
getTag: (functionId: string, tagId: string) => Promise<any>;
deleteTag: (functionId: string, tagId: string) => Promise<any>;
createExecution: (functionId: string, data?: string) => Promise<any>;
listExecutions: (functionId: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC') => Promise<any>;
getExecution: (functionId: string, executionId: string) => Promise<any>;
}
export type SDK = { export type SDK = {
Client: new () => Client; Client: new () => Client;
@ -368,4 +450,5 @@ export type SDK = {
Health: new (client: Client) => HealthClient; Health: new (client: Client) => HealthClient;
Database: new (client: Client) => DatabaseClient; Database: new (client: Client) => DatabaseClient;
Storage: new (client: Client) => StorageClient; Storage: new (client: Client) => StorageClient;
Functions: new (client: Client) => FunctionsClient;
}; };

52
src/appwrite/Functions.ts Normal file
View file

@ -0,0 +1,52 @@
import { Client, Execution, ExecutionList, FunctionsClient, TagList, Vars } from "../appwrite";
import { AppwriteSDK } from '../constants';
import AppwriteCall from '../utils/AppwriteCall';
import { ReadStream } from 'node:fs';
export class Functions {
private readonly functions: FunctionsClient;
constructor(client: Client) {
this.functions = new AppwriteSDK.Functions(client);
}
public async create(name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number): Promise<any> {
return await AppwriteCall(this.functions.create(name, execute, env, vars, events, schedule, timeout));
}
public async list(search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC'): Promise<any> {
return await AppwriteCall(this.functions.list(search, offset, limit, orderType));
}
public async get(functionId: string): Promise<any> {
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<any> {
return await AppwriteCall(this.functions.update(functionId, name, execute, vars, events, schedule, timeout));
}
public async updateTag(functionId: string, tagId: string): Promise<any> {
return await AppwriteCall(this.functions.updateTag(functionId, tagId));
}
public async delete(functionId: string): Promise<void> {
return await AppwriteCall(this.functions.delete(functionId));
}
public async createTag(functionId: string, command: string, code: ReadStream): Promise<any> {
return await AppwriteCall(this.functions.createTag(functionId, command, code));
}
public async listTags(id: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise<TagList | undefined> {
return await AppwriteCall<TagList>(this.functions.listTags(id, search, offset, limit, orderType));
}
public async getTag(functionId: string, tagId: string): Promise<any> {
return await AppwriteCall(this.functions.getTag(functionId, tagId));
}
public async deleteTag(functionId: string, tagId: string): Promise<void> {
return await AppwriteCall(this.functions.deleteTag(functionId, tagId));
}
public async createExecution(functionId: string, data?: string): Promise<any> {
return await AppwriteCall(this.functions.createExecution(functionId, data));
}
public async listExecutions(functionId: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise<ExecutionList | undefined> {
return await AppwriteCall(this.functions.listExecutions(functionId, search, offset, limit, orderType));
}
public async getExecution(functionId: string, executionId: string): Promise<Execution | undefined> {
return await AppwriteCall(this.functions.getExecution(functionId, executionId));
}
}

View file

@ -1,3 +1,4 @@
import { ReadStream } from 'node:fs';
import { Client, FilesList, StorageClient } from "../appwrite"; import { Client, FilesList, StorageClient } from "../appwrite";
import { AppwriteSDK } from '../constants'; import { AppwriteSDK } from '../constants';
import AppwriteCall from "../utils/AppwriteCall"; import AppwriteCall from "../utils/AppwriteCall";
@ -12,4 +13,8 @@ export class Storage {
public async listFiles(): Promise<FilesList | undefined> { public async listFiles(): Promise<FilesList | undefined> {
return await AppwriteCall(this.storage.listFiles()); return await AppwriteCall(this.storage.listFiles());
} }
public async createFile(file: ReadStream): Promise<void> {
return await AppwriteCall(this.storage.createFile(file));
}
} }

View file

@ -1,5 +1,6 @@
import { Client } from "./appwrite"; import { Client } from "./appwrite";
import { Database } from "./appwrite/Database"; import { Database } from "./appwrite/Database";
import { Functions } from './appwrite/Functions';
import { Health } from "./appwrite/Health"; import { Health } from "./appwrite/Health";
import { Storage } from "./appwrite/Storage"; import { Storage } from "./appwrite/Storage";
import { Users } from "./appwrite/Users"; import { Users } from "./appwrite/Users";
@ -12,6 +13,8 @@ export let usersClient: Users | undefined;
export let healthClient: Health | undefined; export let healthClient: Health | undefined;
export let databaseClient: Database | undefined; export let databaseClient: Database | undefined;
export let storageClient: Storage | undefined; export let storageClient: Storage | undefined;
export let functionsClient: Functions | undefined;
function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: AppwriteProjectConfiguration) { function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: AppwriteProjectConfiguration) {
client = new AppwriteSDK.Client(); client = new AppwriteSDK.Client();
@ -22,6 +25,7 @@ function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: Appwrit
healthClient = new Health(client); healthClient = new Health(client);
databaseClient = new Database(client); databaseClient = new Database(client);
storageClient = new Storage(client); storageClient = new Storage(client);
functionsClient = new Functions(client);
return client; return client;
} }
@ -36,4 +40,5 @@ export function createAppwriteClient(config?: AppwriteProjectConfiguration): voi
healthClient = undefined; healthClient = undefined;
databaseClient = undefined; databaseClient = undefined;
storageClient = undefined; storageClient = undefined;
functionsClient = undefined;
} }

View file

@ -0,0 +1,9 @@
import { EditableTreeItem } from '../../tree/common/SimpleEditableTreeItem';
export async function editValue(treeItem: EditableTreeItem): Promise<void> {
if (treeItem === undefined) {
return;
}
await treeItem.prompt();
}

View file

@ -0,0 +1,7 @@
import { functionsClient } from '../../client';
import { TagTreeItem } from '../../tree/functions/tags/TagTreeItem';
export async function activateTag(tagItem: TagTreeItem): Promise<void> {
const tag = tagItem.tag;
await functionsClient?.updateTag(tag.functionId, tag.$id);
}

View file

@ -0,0 +1,11 @@
import { env } from 'vscode';
import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem";
export async function copyExecutionErrors(executionItem: ExecutionTreeItem): Promise<void> {
if (executionItem === undefined) {
return;
}
const execution = executionItem.execution;
env.clipboard.writeText(execution.stderr);
}

View file

@ -0,0 +1,11 @@
import { env } from 'vscode';
import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem";
export async function copyExecutionOutput(executionItem: ExecutionTreeItem): Promise<void> {
if (executionItem === undefined) {
return;
}
const execution = executionItem.execution;
env.clipboard.writeText(execution.stdout);
}

View file

@ -0,0 +1,10 @@
import { functionsClient } from '../../client';
import { ext } from '../../extensionVariables';
import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem';
export async function createExecution(functionTreeItem: FunctionTreeItem): Promise<void> {
const func = functionTreeItem.func;
ext.outputChannel.appendLog(`Creating execution for function ${func.name}`);
await functionsClient?.createExecution(func.$id);
}

View file

@ -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<void> {
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);
}

View file

@ -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<void> {
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);
}
}

View file

@ -0,0 +1,41 @@
import { ProgressLocation, Uri, window } from "vscode";
import { functionsClient, storageClient } from "../../client";
import { getTarReadStream } from "../../utils/tar";
import { ext } from "../../extensionVariables";
import * as fs from "fs";
import { TagsTreeItem } from "../../tree/functions/tags/TagsTreeItem";
import { selectWorkspaceFolder } from "../../utils/workspace";
export async function createTag(item: TagsTreeItem | Uri): Promise<void> {
if (item instanceof Uri) {
window.withProgress({ location: ProgressLocation.Notification, title: "Creating tag..." }, async (_progress, _token) => {
await createTagFromUri(item);
});
return;
}
if (item instanceof TagsTreeItem) {
const folder = await selectWorkspaceFolder("Select folder of your function code.");
console.log(folder);
window.withProgress({ location: ProgressLocation.Notification, title: "Creating tag..." }, async (_progress, _token) => {
await createTagFromUri(Uri.parse(folder));
});
}
}
async function createTagFromUri(uri: Uri): Promise<void> {
const tarFilePath = await getTarReadStream(uri);
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);
}
}

View file

@ -0,0 +1,9 @@
import { functionsClient } from '../../client';
import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem';
export async function deleteFunction(treeItem: FunctionTreeItem): Promise<void> {
if (!treeItem) {
return;
}
await functionsClient?.delete(treeItem.func.$id);
}

View file

@ -0,0 +1,13 @@
import { functionsClient } from '../../client';
import { VarTreeItem } from '../../tree/functions/settings/VarTreeItem';
export async function deleteFunctionVar(treeItem: VarTreeItem): Promise<void> {
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);
}

View file

@ -0,0 +1,11 @@
import { functionsClient } from "../../client";
import { TagTreeItem } from "../../tree/functions/tags/TagTreeItem";
export async function deleteTag(tagItem: TagTreeItem): Promise<void> {
if (tagItem === undefined) {
return;
}
const func = tagItem.parent.parent.func;
await functionsClient?.deleteTag(func.$id, tagItem.tag.$id);
}

View file

@ -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<void> {
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);
}

View file

@ -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<void> {
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);
}

View file

@ -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<void> {
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);
}

View file

@ -0,0 +1,11 @@
import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem";
import { openReadOnlyContent } from "../../ui/openReadonlyContent";
export async function viewExecutionErrors(executionItem: ExecutionTreeItem): Promise<void> {
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');
}

View file

@ -0,0 +1,11 @@
import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem";
import { openReadOnlyContent } from "../../ui/openReadonlyContent";
export async function viewExecutionOutput(executionItem: ExecutionTreeItem): Promise<void> {
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');
}

View file

@ -5,7 +5,8 @@ const documentationLinks = {
users: 'https://appwrite.io/docs/server/users', users: 'https://appwrite.io/docs/server/users',
database: 'https://appwrite.io/docs/client/database', database: 'https://appwrite.io/docs/client/database',
health: 'https://appwrite.io/docs/server/health', 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; type DocsPage = keyof typeof documentationLinks;

View file

@ -25,6 +25,22 @@ import { viewUserPrefs } from "./users/viewUserPrefs";
import { editPermission } from "./database/permissions/editPermission"; import { editPermission } from "./database/permissions/editPermission";
import { setActiveProject } from "./project/setActiveProject"; import { setActiveProject } from "./project/setActiveProject";
import { removeProject } from "./project/removeProject"; 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 { class CommandRegistrar {
constructor(private readonly context: ExtensionContext) {} constructor(private readonly context: ExtensionContext) {}
@ -56,6 +72,9 @@ export function registerCommands(context: ExtensionContext): void {
}); });
}; };
/** Common **/
registerCommand("editValue", editValue);
/** General **/ /** General **/
registerCommand("Connect", connectAppwrite, "all"); registerCommand("Connect", connectAppwrite, "all");
@ -98,4 +117,23 @@ export function registerCommands(context: ExtensionContext): void {
registerCommand("setActiveProject", setActiveProject, "all"); registerCommand("setActiveProject", setActiveProject, "all");
registerCommand("refreshProjects", undefined, "projects"); registerCommand("refreshProjects", undefined, "projects");
registerCommand("removeProject", removeProject, "all"); registerCommand("removeProject", removeProject, "all");
/** 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);
} }

View file

@ -3,7 +3,7 @@ import { commands, Uri } from "vscode";
import { clientConfig } from "../../client"; import { clientConfig } from "../../client";
import { UserTreeItem } from "../../tree/users/UserTreeItem"; import { UserTreeItem } from "../../tree/users/UserTreeItem";
function getConsoleUrlFromEndpoint(endpoint: string): string { export function getConsoleUrlFromEndpoint(endpoint: string): string {
const url = new URL(endpoint); const url = new URL(endpoint);
return `${url.origin}/console`; return `${url.origin}/console`;
} }

View file

@ -1,4 +1,197 @@
import type { SDK } from './appwrite'; import type { SDK } from "./appwrite";
// eslint-disable-next-line @typescript-eslint/no-var-requires // 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",
];

View file

@ -4,6 +4,7 @@ import { registerCommands } from "./commands/registerCommands";
import { ext } from "./extensionVariables"; import { ext } from "./extensionVariables";
import { getActiveProjectConfiguration } from "./settings"; import { getActiveProjectConfiguration } from "./settings";
import { DatabaseTreeItemProvider } from "./tree/database/DatabaseTreeItemProvider"; import { DatabaseTreeItemProvider } from "./tree/database/DatabaseTreeItemProvider";
import { FunctionsTreeItemProvider } from './tree/functions/FunctionsTreeItemProvider';
import { HealthTreeItemProvider } from "./tree/health/HealthTreeItemProvider"; import { HealthTreeItemProvider } from "./tree/health/HealthTreeItemProvider";
import { ProjectsTreeItemProvider } from "./tree/projects/ProjectsTreeItemProvider"; import { ProjectsTreeItemProvider } from "./tree/projects/ProjectsTreeItemProvider";
import { StorageTreeItemProvider } from "./tree/storage/StorageTreeItemProvider"; import { StorageTreeItemProvider } from "./tree/storage/StorageTreeItemProvider";
@ -16,12 +17,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const databaseTreeItemProvider = new DatabaseTreeItemProvider(); const databaseTreeItemProvider = new DatabaseTreeItemProvider();
const storageTreeItemProvider = new StorageTreeItemProvider(); const storageTreeItemProvider = new StorageTreeItemProvider();
const projectsTreeItemProvider = new ProjectsTreeItemProvider(); const projectsTreeItemProvider = new ProjectsTreeItemProvider();
const functionsTreeItemProvider = new FunctionsTreeItemProvider();
vscode.window.registerTreeDataProvider("Users", userTreeItemProvider); vscode.window.registerTreeDataProvider("Users", userTreeItemProvider);
vscode.window.registerTreeDataProvider("Health", healthTreeItemProvider); vscode.window.registerTreeDataProvider("Health", healthTreeItemProvider);
vscode.window.registerTreeDataProvider("Database", databaseTreeItemProvider); vscode.window.registerTreeDataProvider("Database", databaseTreeItemProvider);
vscode.window.registerTreeDataProvider("Storage", storageTreeItemProvider); vscode.window.registerTreeDataProvider("Storage", storageTreeItemProvider);
vscode.window.registerTreeDataProvider("Projects", projectsTreeItemProvider); vscode.window.registerTreeDataProvider("Projects", projectsTreeItemProvider);
vscode.window.registerTreeDataProvider("Functions", functionsTreeItemProvider);
const activeProject = await getActiveProjectConfiguration(); const activeProject = await getActiveProjectConfiguration();
createAppwriteClient(activeProject); createAppwriteClient(activeProject);
@ -35,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
database: databaseTreeItemProvider, database: databaseTreeItemProvider,
storage: storageTreeItemProvider, storage: storageTreeItemProvider,
projects: projectsTreeItemProvider, projects: projectsTreeItemProvider,
functions: functionsTreeItemProvider
}; };
registerCommands(context); registerCommands(context);

View file

@ -1,5 +1,6 @@
import { ExtensionContext } from "vscode"; import { ExtensionContext } from "vscode";
import { DatabaseTreeItemProvider } from './tree/database/DatabaseTreeItemProvider'; import { DatabaseTreeItemProvider } from './tree/database/DatabaseTreeItemProvider';
import { FunctionsTreeItemProvider } from './tree/functions/FunctionsTreeItemProvider';
import { HealthTreeItemProvider } from './tree/health/HealthTreeItemProvider'; import { HealthTreeItemProvider } from './tree/health/HealthTreeItemProvider';
import { ProjectsTreeItemProvider } from './tree/projects/ProjectsTreeItemProvider'; import { ProjectsTreeItemProvider } from './tree/projects/ProjectsTreeItemProvider';
import { StorageTreeItemProvider } from './tree/storage/StorageTreeItemProvider'; import { StorageTreeItemProvider } from './tree/storage/StorageTreeItemProvider';
@ -12,12 +13,13 @@ export type AppwriteTree = {
database?: DatabaseTreeItemProvider; database?: DatabaseTreeItemProvider;
storage?: StorageTreeItemProvider; storage?: StorageTreeItemProvider;
projects?: ProjectsTreeItemProvider; projects?: ProjectsTreeItemProvider;
functions?: FunctionsTreeItemProvider;
}; };
export type Ext = { export type Ext = {
context?: ExtensionContext; context?: ExtensionContext;
outputChannel?: AppwriteOutputChannel; outputChannel: AppwriteOutputChannel;
tree?: AppwriteTree; tree?: AppwriteTree;
}; };
export const ext: Ext = {}; export const ext: Ext = {} as Ext;

View file

@ -0,0 +1,13 @@
import { TreeItem } from "vscode";
export abstract class EditableTreeItemBase<T> extends TreeItem {
public abstract setValue(value: T): Promise<void>;
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<void>;
}

View file

@ -0,0 +1,38 @@
import { QuickPickItem, QuickPickOptions, window } from "vscode";
import { EditableTreeItemBase } from "./EditableTreeItemBase";
export abstract class EnumEditableTreeItemBase extends EditableTreeItemBase<string[]> {
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<void> {
const value = await window.showQuickPick(
this.options.map<QuickPickItem>((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));
}
}
}

View file

@ -0,0 +1,18 @@
import { TreeItem, window } from "vscode";
export class EditableTreeItem extends TreeItem {
public readonly setValue: (value: string) => Promise<void>;
constructor(label: string, contextValuePrefix: string, public readonly value: string, setValue: (value: string) => Promise<void>) {
super(label);
this.setValue = setValue;
this.contextValue = `editable_${contextValuePrefix}`;
}
public async prompt(): Promise<void> {
const value = await window.showInputBox({ value: this.value });
if (value !== undefined) {
this.setValue(value);
}
}
}

View file

@ -0,0 +1,22 @@
import { InputBoxOptions, window } from "vscode";
import { EditableTreeItemBase } from "./EditableTreeItemBase";
export abstract class StringEditableTreeItemBase extends EditableTreeItemBase<string> {
public abstract setValue(value: string): Promise<void>;
public inputBoxOptions: InputBoxOptions;
constructor(contextValuePrefix: string, public readonly value: string, description?: string) {
super(contextValuePrefix, value, description);
this.inputBoxOptions = {
prompt: description,
};
}
public async prompt(): Promise<void> {
const value = await window.showInputBox({ value: this.value, ...this.inputBoxOptions });
if (value !== undefined) {
this.setValue(value);
}
}
}

View file

@ -41,11 +41,11 @@ export class DatabaseTreeItemProvider implements vscode.TreeDataProvider<vscode.
const collectionsList = await AppwriteCall<CollectionsList, CollectionsList>(databaseSdk.listCollections()); const collectionsList = await AppwriteCall<CollectionsList, CollectionsList>(databaseSdk.listCollections());
if (collectionsList) { if (collectionsList) {
const userTreeItems = collectionsList.collections.map((collection: Collection) => new CollectionTreeItem(collection, this)) ?? []; const collectionTreeItems = collectionsList.collections.map((collection: Collection) => new CollectionTreeItem(collection, this)) ?? [];
const headerItem: vscode.TreeItem = { const headerItem: vscode.TreeItem = {
label: `Total collections: ${collectionsList.sum}`, label: `Total collections: ${collectionsList.sum}`,
}; };
return [headerItem, ...userTreeItems]; return [headerItem, ...collectionTreeItems];
} }
return [{ label: "No collections found" }]; return [{ label: "No collections found" }];

View file

@ -0,0 +1,30 @@
import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Function } from "../../appwrite";
import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase";
import { msToDate } from '../../utils/date';
import { ExecutionsTreeItem } from './executions/ExecutionsTreeItem';
import { FunctionsTreeItemProvider } from './FunctionsTreeItemProvider';
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<TreeItem[]> {
return [new FunctionSettingsTreeItem(this), new TagsTreeItem(this), new ExecutionsTreeItem(this)];
}
public async refresh(): Promise<void> {
this.provider.refreshChild(this);
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
contextValue = "function";
iconPath = new ThemeIcon("symbol-event");
}

View file

@ -0,0 +1,52 @@
import * as vscode from "vscode";
import { client } from "../../client";
import { Function, FunctionsList } from "../../appwrite";
import { AppwriteSDK } from "../../constants";
import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase";
import { ext } from "../../extensionVariables";
import { EventEmitter, TreeItem } from "vscode";
import { FunctionTreeItem } from "./FunctionTreeItem";
export class FunctionsTreeItemProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private _onDidChangeTreeData: EventEmitter<TreeItem | undefined | void> = new EventEmitter<TreeItem | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | void> = this._onDidChangeTreeData.event;
refresh(): void {
ext.outputChannel?.appendLine("Refreshing functions tree provider...");
this._onDidChangeTreeData.fire();
}
refreshChild(child: vscode.TreeItem): void {
this._onDidChangeTreeData.fire(child);
}
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
return element;
}
async getChildren(parent?: AppwriteTreeItemBase | TreeItem): Promise<vscode.TreeItem[]> {
if (client === undefined) {
return Promise.resolve([]);
}
if (parent === undefined) {
const functionsSdk = new AppwriteSDK.Functions(client);
const list: FunctionsList = await functionsSdk.list();
if (list) {
const functionTreeItems = list.functions.map((func: Function) => new FunctionTreeItem(func, this)) ?? [];
return functionTreeItems;
}
return [{ label: "No functions found" }];
}
if (parent instanceof AppwriteTreeItemBase) {
return parent.getChildren?.() ?? [];
}
return [];
}
}

View file

@ -0,0 +1,86 @@
import { MarkdownString, ThemeColor, ThemeIcon, TreeItem } from "vscode";
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<ExecutionStatus, ThemeIcon> = {
processing: new ThemeIcon("loading"),
waiting: new ThemeIcon("circle-outline"),
completed: new ThemeIcon("circle-filled", new ThemeColor("testing.iconPassed")),
failed: new ThemeIcon("circle-filled", new ThemeColor("testing.iconFailed")),
};
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export class ExecutionTreeItem extends TreeItem {
public isAutoRefreshing: boolean = false;
private refreshCount: number = 0;
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();
}
async autoRefresh(): Promise<void> {
if (!this.isAutoRefreshing) {
return;
}
this.refreshCount++;
ext.outputChannel.appendLog("Refreshing execution.");
const execution = await functionsClient?.getExecution(this.parent.parent.func.$id, this.execution.$id);
if (!execution) {
ext.outputChannel.appendLog("Execution is undefined");
this.isAutoRefreshing = false;
return;
}
this.contextValue = this.getContextValue(execution);
this.iconPath = executionStatusIcons[execution.status];
this.label = this.getLabel(execution);
this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting";
ext.tree?.functions?.refreshChild(this);
await sleep(1000);
this.autoRefresh();
}
getLabel(execution: Execution): string {
if (execution.status === "completed" || execution.status === "failed") {
return `${this.getCreated(execution)} (${execution.time.toPrecision(2)}s)`;
}
return `${this.getCreated(execution)} (${execution.status})`;
}
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";
}
getCreated(execution: Execution): string {
return msToDate(execution.dateCreated);
}
}

View file

@ -0,0 +1,27 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Execution, ExecutionList } from '../../../appwrite';
import { functionsClient } from "../../../client";
import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase';
import { ExecutionTreeItem } from './ExecutionTreeItem';
import { FunctionTreeItem } from '../FunctionTreeItem';
export class ExecutionsTreeItem extends AppwriteTreeItemBase<FunctionTreeItem> {
constructor(public readonly parent: FunctionTreeItem) {
super(parent, "Executions");
}
public async getChildren(): Promise<TreeItem[]> {
if (!functionsClient) {
return [];
}
const executions: ExecutionList | undefined = await functionsClient.listExecutions(this.parent.func.$id, undefined, undefined, undefined, 'DESC');
const children = executions?.executions.map((execution: Execution) => new ExecutionTreeItem(this, execution)) ?? [new TreeItem('No exeuctions.')];
return children;
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
contextValue = "executions";
iconPath = new ThemeIcon("history");
}

View file

@ -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<void> {
await functionsClient?.update(this.func.$id, this.func.name, [], this.func.vars, value, this.func.schedule, this.func.timeout);
ext.tree?.functions?.refresh();
}
}

View file

@ -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<FunctionTreeItem> {
public readonly func: Function;
constructor(public readonly parent: FunctionTreeItem) {
super(parent, "Settings");
this.func = parent.func;
}
public async getChildren(): Promise<TreeItem[]> {
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");
}

View file

@ -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<void> {
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);
}
}

View file

@ -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<void> {
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";
}
}
}

View file

@ -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<void> {
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`;
}
}

View file

@ -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<void> {
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<void> {
const keyval = await keyValuePrompt(this.key, this.value);
if (keyval) {
this.setValue(keyval.value, keyval.key);
}
}
}

View file

@ -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<FunctionSettingsTreeItem> {
public readonly vars: Vars;
constructor(parent: FunctionSettingsTreeItem) {
super(parent, "Environment variables");
this.vars = parent.func.vars;
this.description = undefined;
}
public async getChildren(): Promise<TreeItem[]> {
return Object.keys(this.vars).map((key) => new VarTreeItem(this, key, this.vars[key]));
}
contextValue = "vars";
collapsibleState = TreeItemCollapsibleState.Collapsed;
}

View file

@ -0,0 +1,17 @@
import { MarkdownString, ThemeIcon, TreeItem } from "vscode";
import { Tag } from '../../../appwrite';
import { msToDate } from '../../../utils/date';
import { TagsTreeItem } from './TagsTreeItem';
export class TagTreeItem extends TreeItem {
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 = `${msToDate(tag.dateCreated)}${active ? ' (Active)' : ''}`;
this.description = tag.$id;
this.iconPath = new ThemeIcon(active ? 'circle-filled' : 'circle-outline');
this.contextValue = `tag${active ? '_active' : ''}`;
this.tooltip = new MarkdownString(`ID: ${tag.$id} \nCreated: ${msToDate(tag.dateCreated)} \nCommand: ${tag.command} \nSize: ${tag.size}B`);
}
}

View file

@ -0,0 +1,25 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { functionsClient } from "../../../client";
import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase';
import { FunctionTreeItem } from '../FunctionTreeItem';
import { TagTreeItem } from './TagTreeItem';
export class TagsTreeItem extends AppwriteTreeItemBase<FunctionTreeItem> {
constructor(public readonly parent: FunctionTreeItem) {
super(parent, "Tags");
}
public async getChildren(): Promise<TreeItem[]> {
if (!functionsClient) {
return [];
}
const tags = await functionsClient.listTags(this.parent.func.$id);
return tags?.tags.sort((a, b) => b.dateCreated - a.dateCreated).map((tag) => new TagTreeItem(this, tag)) ?? [new TreeItem('No tags.')];
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
contextValue = "tags";
iconPath = new ThemeIcon("tag");
}

View file

@ -10,7 +10,7 @@ export default function AppwriteCall<T, R = T>(
): Promise<R | undefined> { ): Promise<R | undefined> {
return promise.then( return promise.then(
(successResp) => { (successResp) => {
ext.outputChannel?.appendLog("Appwrite call success"); ext.outputChannel?.appendLog(`Appwrite call success:`);
if (onSuccess) { if (onSuccess) {
return onSuccess((successResp as unknown) as T); return onSuccess((successResp as unknown) as T);
} }

5
src/utils/date.ts Normal file
View file

@ -0,0 +1,5 @@
import dayjs = require('dayjs');
export function msToDate(ms: number): string {
return dayjs(ms).format("LTS");
}

31
src/utils/tar.ts Normal file
View file

@ -0,0 +1,31 @@
import tar = require("tar");
import { Uri, window, workspace } from "vscode";
import { ext } from "../extensionVariables";
import * as path from "path";
import * as os from "os";
import * as fs from "fs";
export async function getTarReadStream(folder: Uri): Promise<string | undefined> {
try {
const folderName = path.basename(folder.path);
const tarName = `${folderName}.tar.gz`;
const cwd = path.resolve(folder.fsPath, '..');
if (cwd === undefined) {
window.showErrorMessage("No workspace open.");
return;
}
ext.outputChannel.appendLog(`Creating '${tarName}' in '${workspace.workspaceFolders?.[0].uri.fsPath}'...`);
const tarFilePath = path.join(os.tmpdir(), tarName);
tar.create({ gzip: true, cwd: cwd }, [path.relative(cwd, folder.fsPath)]).pipe(fs.createWriteStream(tarFilePath));
ext.outputChannel.appendLog(`Created ${tarFilePath}`);
return tarFilePath;
} catch (e) {
ext.outputChannel?.appendLog("Error creating tar.gz: " + e);
}
}

0
src/utils/validation.ts Normal file
View file

84
src/utils/workspace.ts Normal file
View file

@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from "path";
import * as vscode from "vscode";
import { QuickPickItem } from 'vscode';
export interface IAzureQuickPickItem<T = undefined> extends QuickPickItem {
/**
* An optional id to uniquely identify this item across sessions, used in persisting previous selections
* If not specified, a hash of the label will be used
*/
id?: string;
data: T;
/**
* Callback to use when this item is picked, instead of returning the pick
* Only applies when used as part of an `AzureWizard`
* This is not compatible with `canPickMany`
*/
onPicked?: () => void | Promise<void>;
/**
* The group that this pick belongs to. Set `IAzureQuickPickOptions.enableGrouping` for this property to take effect
* Only applies when used as part of an `AzureWizard`
*/
group?: string;
/**
* Optionally used to suppress persistence for this item, defaults to `false`
*/
suppressPersistence?: boolean;
}
export async function selectWorkspaceFolder(placeHolder: string): Promise<string> {
return await selectWorkspaceItem(placeHolder, {
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri:
vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0
? vscode.workspace.workspaceFolders[0].uri
: undefined,
openLabel: "Select",
});
}
export async function selectWorkspaceFile(placeHolder: string, fileExtensions?: string[]): Promise<string> {
const filters: { [name: string]: string[] } = {};
if (fileExtensions) {
filters.Artifacts = fileExtensions;
}
return await selectWorkspaceItem(placeHolder, {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: "Select",
filters: filters,
});
}
export async function selectWorkspaceItem(placeHolder: string, options: vscode.OpenDialogOptions): Promise<string> {
let folder: IAzureQuickPickItem<string | undefined> | undefined;
if (vscode.workspace.workspaceFolders) {
const folderPicks: IAzureQuickPickItem<string | undefined>[] = await Promise.all(
vscode.workspace.workspaceFolders.map((f: vscode.WorkspaceFolder) => {
return { label: path.basename(f.uri.fsPath), description: f.uri.fsPath, data: f.uri.fsPath };
})
);
folderPicks.push({ label: "$(file-directory) Browse...", description: "", data: undefined });
folder = await vscode.window.showQuickPick(folderPicks, { placeHolder });
}
if (folder?.data) {
return folder.data;
} else {
return (await vscode.window.showOpenDialog(options))?.[0].fsPath ?? '';
}
}