Basic functions create tag and create execution feature

This commit is contained in:
alexweininger 2021-05-29 00:55:12 -05:00
parent b4e5fdcd20
commit d57c1e9696
19 changed files with 564 additions and 4557 deletions

4636
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,9 @@
"onView:Users",
"onView:Database",
"onView:Health",
"onCommand:vscode-appwrite.AddProject"
"onView:Functions",
"onCommand:vscode-appwrite.AddProject",
"onCommand:vscode-appwrite.CreateTag"
],
"main": "./dist/extension.js",
"contributes": {
@ -188,6 +190,16 @@
"command": "vscode-appwrite.removeProject",
"title": "Remove project",
"icon": "$(trash)"
},
{
"command": "vscode-appwrite.CreateTag",
"title": "Create function tag",
"icon": "$(cloud-upload)"
},
{
"command": "vscode-appwrite.CreateExecution",
"title": "Execute function",
"icon": "$(play)"
}
],
"views": {
@ -211,6 +223,10 @@
{
"id": "Projects",
"name": "Projects"
},
{
"id": "Functions",
"name": "Functions"
}
]
},
@ -383,6 +399,18 @@
{
"command": "vscode-appwrite.removeProject",
"when": "viewItem =~ /(appwriteProject)/"
},
{
"command": "vscode-appwrite.CreateExecution",
"when": "viewItem =~ /(function)/",
"group": "inline"
}
],
"explorer/context": [
{
"command": "vscode-appwrite.CreateTag",
"when": "explorerResourceIsFolder == true",
"group": "appwrite@1"
}
],
"commandPalette": [
@ -499,6 +527,7 @@
"@types/glob": "^7.1.3",
"@types/mocha": "^8.0.4",
"@types/node": "^12.11.7",
"@types/tar": "^4.0.4",
"@types/vscode": "^1.55.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
@ -515,6 +544,7 @@
"dependencies": {
"dayjs": "^1.10.4",
"fs-extra": "^9.1.0",
"node-appwrite": "^2.2.1"
"node-appwrite": "^2.2.1",
"tar": "^6.1.0"
}
}

88
src/appwrite.d.ts vendored
View file

@ -1,3 +1,6 @@
import { ReadStream } from 'fs';
import { Stream } from 'node:stream';
export type Token = {
/**
* Token ID.
@ -287,7 +290,7 @@ export type Rule = {
list: string[];
};
interface Permissions {
export type Permissions = {
read: string[];
write: string[];
}
@ -361,6 +364,88 @@ export type StorageClient = {
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 = {
Client: new () => Client;
@ -368,4 +453,5 @@ export type SDK = {
Health: new (client: Client) => HealthClient;
Database: new (client: Client) => DatabaseClient;
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,5 +1,6 @@
import { Client } from "./appwrite";
import { Database } from "./appwrite/Database";
import { Functions } from './appwrite/Functions';
import { Health } from "./appwrite/Health";
import { Storage } from "./appwrite/Storage";
import { Users } from "./appwrite/Users";
@ -12,6 +13,8 @@ export let usersClient: Users | undefined;
export let healthClient: Health | undefined;
export let databaseClient: Database | undefined;
export let storageClient: Storage | undefined;
export let functionsClient: Functions | undefined;
function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: AppwriteProjectConfiguration) {
client = new AppwriteSDK.Client();
@ -22,6 +25,7 @@ function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: Appwrit
healthClient = new Health(client);
databaseClient = new Database(client);
storageClient = new Storage(client);
functionsClient = new Functions(client);
return client;
}
@ -36,4 +40,5 @@ export function createAppwriteClient(config?: AppwriteProjectConfiguration): voi
healthClient = undefined;
databaseClient = undefined;
storageClient = undefined;
functionsClient = undefined;
}

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,14 @@
import { Uri } from 'vscode';
import { functionsClient } from '../../client';
import { getTarReadStream } from '../../utils/tar';
import { ext } from '../../extensionVariables';
export async function createTag(folder: Uri): Promise<void> {
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);
}
}
}

View file

@ -25,6 +25,8 @@ import { viewUserPrefs } from "./users/viewUserPrefs";
import { editPermission } from "./database/permissions/editPermission";
import { setActiveProject } from "./project/setActiveProject";
import { removeProject } from "./project/removeProject";
import { createTag } from './functions/createTag';
import { createExecution } from './functions/createExecution';
class CommandRegistrar {
constructor(private readonly context: ExtensionContext) {}
@ -98,4 +100,8 @@ export function registerCommands(context: ExtensionContext): void {
registerCommand("setActiveProject", setActiveProject, "all");
registerCommand("refreshProjects", undefined, "projects");
registerCommand("removeProject", removeProject, "all");
/** Functions **/
registerCommand("CreateTag", createTag, "functions");
registerCommand("CreateExecution", createExecution, "functions");
}

View file

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

View file

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

View file

@ -41,11 +41,11 @@ export class DatabaseTreeItemProvider implements vscode.TreeDataProvider<vscode.
const collectionsList = await AppwriteCall<CollectionsList, CollectionsList>(databaseSdk.listCollections());
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 = {
label: `Total collections: ${collectionsList.sum}`,
};
return [headerItem, ...userTreeItems];
return [headerItem, ...collectionTreeItems];
}
return [{ label: "No collections found" }];

View file

@ -0,0 +1,73 @@
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 { 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, private 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.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.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") {
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");
}
contextValue = "tag";
}

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,26 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Function } from "../../appwrite";
import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase";
import { ExecutionsTreeItem } from './ExecutionsTreeItem';
import { FunctionsTreeItemProvider } from './FunctionsTreeItemProvider';
import { TagsTreeItem } from './TagsTreeItem';
export class FunctionTreeItem extends AppwriteTreeItemBase {
constructor(public func: Function, public readonly provider: FunctionsTreeItemProvider) {
super(undefined, func.name);
}
public async getChildren(): Promise<TreeItem[]> {
return [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,55 @@
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)) ?? [];
const headerItem: vscode.TreeItem = {
label: `Total functions: ${list.sum}`,
};
return [headerItem, ...functionTreeItems];
}
return [{ label: "No functions found" }];
}
if (parent instanceof AppwriteTreeItemBase) {
return parent.getChildren?.() ?? [];
}
return [];
}
}

View file

@ -0,0 +1,16 @@
import { ThemeIcon, TreeItem } from "vscode";
import { Tag } from '../../appwrite';
import { TagsTreeItem } from './TagsTreeItem';
export class TagTreeItem extends TreeItem {
constructor(public readonly parent: TagsTreeItem, 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');
}
contextValue = "tag";
}

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> {
return promise.then(
(successResp) => {
ext.outputChannel?.appendLog("Appwrite call success");
ext.outputChannel?.appendLog(`Appwrite call success:`);
if (onSuccess) {
return onSuccess((successResp as unknown) as T);
}

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

@ -0,0 +1,38 @@
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";
import { ReadStream } from 'node:fs';
export async function getTarReadStream(folder: Uri): Promise<ReadStream | undefined> {
try {
const folderName = path.basename(folder.path);
const tarName = `${folderName}.tar.gz`;
const cwd = 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, { emitClose: true}));
const stream = fs.createReadStream(tarFilePath);
stream.on('close', () => {
try {
fs.unlinkSync(tarFilePath);
} catch (e) {
//
}
});
return stream;
} catch (e) {
ext.outputChannel?.appendLog("Error creating tar.gz: " + e);
}
}