diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c7b68..6052fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +## Added +- You can now easily create function tags from multiple places in the extension. + ## [0.1.0] - 2021-5-29 ## Functions! diff --git a/package-lock.json b/package-lock.json index 657b2b0..2484cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-appwrite", - "version": "0.1.0", + "version": "0.0.9", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9055a1b..63f60e4 100644 --- a/package.json +++ b/package.json @@ -40,151 +40,187 @@ { "command": "vscode-appwrite.Connect", "title": "Connect to Appwrite", - "category": "" + "category": "Appwrite" }, { "command": "vscode-appwrite.CreateUser", "title": "Create user", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.DeleteUser", "title": "Delete user", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.GetUserLogs", "title": "Get user logs", - "icon": "$(output)" + "icon": "$(output)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openUserInConsole", "title": "View user in Appwrite console", - "icon": "$(link-external)" + "icon": "$(link-external)", + "category": "Appwrite" }, { "command": "vscode-appwrite.viewUserPrefs", "title": "View user preferences", - "icon": "$(json)" + "icon": "$(json)", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshEntry", "title": "Refresh", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.copyUserId", "title": "Copy user ID", - "icon": "$(clippy)" + "icon": "$(clippy)", + "category": "Appwrite" }, { "command": "vscode-appwrite.copyUserEmail", "title": "Copy user email", - "icon": "$(clippy)" + "icon": "$(clippy)", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshUsersList", "title": "Refresh users list", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.OpenUsersDocumentation", "title": "Open documentation", - "icon": "$(book)" + "icon": "$(book)", + "category": "Appwrite" }, { "command": "vscode-appwrite.OpenDatabaseDocumentation", "title": "Open documentation", - "icon": "$(book)" + "icon": "$(book)", + "category": "Appwrite" }, { "command": "vscode-appwrite.viewDocumentAsJson", - "title": "View as JSON" + "title": "View as JSON", + "category": "Appwrite" }, { "command": "vscode-appwrite.viewCollectionAsJson", - "title": "View collection as JSON" + "title": "View collection as JSON", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshCollection", "title": "Refresh", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.createRule", "title": "Create collection rule", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.removeRule", - "title": "Remove collection rule" + "title": "Remove collection rule", + "category": "Appwrite" }, { "command": "vscode-appwrite.deleteDocument", - "title": "Delete document" + "title": "Delete document", + "category": "Appwrite" }, { "command": "vscode-appwrite.deleteCollection", - "title": "Delete collection" + "title": "Delete collection", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshCollectionsList", "title": "Refresh", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.createCollection", "title": "Create collection", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.createPermission", "title": "Create permission", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.deletePermission", "title": "Delete permission", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.editPermission", "title": "Edit permission", - "icon": "$(edit)" + "icon": "$(edit)", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshHealth", "title": "Refresh health", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openHealthDocumentation", "title": "Open health documentation", - "icon": "$(book)" + "icon": "$(book)", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshStorage", "title": "Refresh storage", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openStorageDocumentation", "title": "Open storage documentation", - "icon": "$(book)" + "icon": "$(book)", + "category": "Appwrite" }, { "command": "vscode-appwrite.addProject", "title": "Add Appwrite project", - "icon": "$(plus)" + "icon": "$(plus)", + "category": "Appwrite" }, { "command": "vscode-appwrite.setActiveProject", - "title": "Set as active" + "title": "Set as active", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshProjects", "title": "Refresh projects", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Appwrite" + }, + { + "command": "vscode-appwrite.refreshFunctions", + "title": "Refresh functions", + "icon": "$(refresh)", + "category": "Appwrite" }, { "command": "vscode-appwrite.refreshFunctions", @@ -194,91 +230,115 @@ { "command": "vscode-appwrite.removeProject", "title": "Remove project", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.CreateTag", "title": "Create function tag", - "icon": "$(cloud-upload)" + "icon": "$(cloud-upload)", + "shortTitle": "Create function tag", + "category": "Appwrite" }, { "command": "vscode-appwrite.deleteTag", "title": "Delete tag", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.CreateExecution", - "title": "Execute" + "title": "Execute", + "category": "Appwrite" }, { "command": "vscode-appwrite.activateTag", - "title": "Activate" + "title": "Activate", + "category": "Appwrite" }, { "command": "vscode-appwrite.editValue", "title": "Edit", - "icon": "$(edit)" + "icon": "$(edit)", + "category": "Appwrite" }, { "command": "vscode-appwrite.deleteFunction", "title": "Delete", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openFunctionsDocumentation", "title": "Open functions documentation", - "icon": "$(book)" + "icon": "$(book)", + "category": "Appwrite" }, { "command": "vscode-appwrite.createFunction", "title": "Create function", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.createFunctionVar", "title": "Create variable", - "icon": "$(add)" + "icon": "$(add)", + "category": "Appwrite" }, { "command": "vscode-appwrite.deleteFunctionVar", "title": "Delete variable", - "icon": "$(trash)" + "icon": "$(trash)", + "category": "Appwrite" }, { "command": "vscode-appwrite.viewExecutionOutput", "title": "View execution stdout", - "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/" + "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/", + "category": "Appwrite" }, { "command": "vscode-appwrite.viewExecutionErrors", "title": "View execution stderr", - "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/" + "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/", + "category": "Appwrite" }, { "command": "vscode-appwrite.copyExecutionOutput", "title": "Copy execution stdout", - "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/" + "enablement": "viewItem =~ /^((execution|execution_outputOnly))$/", + "category": "Appwrite" }, { "command": "vscode-appwrite.copyExecutionErrors", "title": "Copy execution stderr", - "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/" + "enablement": "viewItem =~ /^((execution|execution_errorOnly))$/", + "category": "Appwrite" }, { "command": "vscode-appwrite.openExecutionsInBrowser", "title": "View executions in browser", "enablement": "viewItem =~ /^(executions)$/", - "icon": "$(link-external)" + "icon": "$(link-external)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openFunctionTagsInBrowser", "title": "Open function tags in browser", - "icon": "$(link-external)" + "icon": "$(link-external)", + "category": "Appwrite" }, { "command": "vscode-appwrite.openFunctionSettingsInBrowser", "title": "Open function settings in browser", - "icon": "$(link-external)" + "icon": "$(link-external)", + "category": "Appwrite" + }, + { + "command": "vscode-appwrite.viewMore", + "title": "View more", + "category": "Appwrite" } ], "views": { @@ -570,6 +630,9 @@ } ], "commandPalette": [ + { + "command": "vscode-appwrite.CreateTag" + }, { "command": "vscode-appwrite.Connect" }, diff --git a/src/appwrite/Functions.ts b/src/appwrite/Functions.ts index 2fb0e6a..1bd8ce9 100644 --- a/src/appwrite/Functions.ts +++ b/src/appwrite/Functions.ts @@ -1,10 +1,10 @@ -import { Client, Execution, ExecutionList, FunctionsClient, TagList, Vars } from "../appwrite"; +import { Client, Execution, ExecutionList, FunctionsClient, FunctionsList, Tag, 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; + public readonly functions: FunctionsClient; constructor(client: Client) { this.functions = new AppwriteSDK.Functions(client); @@ -13,7 +13,7 @@ export class Functions { public async create(name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number): Promise { 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 { + public async list(search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC'): Promise { return await AppwriteCall(this.functions.list(search, offset, limit, orderType)); } public async get(functionId: string): Promise { @@ -28,7 +28,7 @@ export class Functions { public async delete(functionId: string): Promise { return await AppwriteCall(this.functions.delete(functionId)); } - public async createTag(functionId: string, command: string, code: ReadStream): Promise { + public async createTag(functionId: string, command: string, code: ReadStream): Promise { return await AppwriteCall(this.functions.createTag(functionId, command, code)); } public async listTags(id: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise { @@ -40,11 +40,11 @@ export class Functions { public async deleteTag(functionId: string, tagId: string): Promise { return await AppwriteCall(this.functions.deleteTag(functionId, tagId)); } - public async createExecution(functionId: string, data?: string): Promise { + public async createExecution(functionId: string, data?: string): Promise { return await AppwriteCall(this.functions.createExecution(functionId, data)); } public async listExecutions(functionId: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise { - return await AppwriteCall(this.functions.listExecutions(functionId, search, offset, limit, orderType)); + return await AppwriteCall(this.functions.listExecutions(functionId, search, limit, offset, orderType)); } public async getExecution(functionId: string, executionId: string): Promise { return await AppwriteCall(this.functions.getExecution(functionId, executionId)); diff --git a/src/commands/common/editValue.ts b/src/commands/common/editValue.ts index 92a101f..d7396f8 100644 --- a/src/commands/common/editValue.ts +++ b/src/commands/common/editValue.ts @@ -1,4 +1,4 @@ -import { EditableTreeItem } from '../../tree/common/SimpleEditableTreeItem'; +import { EditableTreeItem } from '../../tree/common/editable/SimpleEditableTreeItem'; export async function editValue(treeItem: EditableTreeItem): Promise { if (treeItem === undefined) { diff --git a/src/commands/common/viewMore.ts b/src/commands/common/viewMore.ts new file mode 100644 index 0000000..736521f --- /dev/null +++ b/src/commands/common/viewMore.ts @@ -0,0 +1,5 @@ +import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; + +export async function viewMore(treeItem: AppwriteTreeItemBase): Promise { + await treeItem.viewMore(); +} diff --git a/src/commands/functions/activateTag.ts b/src/commands/functions/activateTag.ts index 534a05a..b34a423 100644 --- a/src/commands/functions/activateTag.ts +++ b/src/commands/functions/activateTag.ts @@ -1,7 +1,8 @@ +import { Tag } from '../../appwrite'; import { functionsClient } from '../../client'; import { TagTreeItem } from '../../tree/functions/tags/TagTreeItem'; -export async function activateTag(tagItem: TagTreeItem): Promise { - const tag = tagItem.tag; +export async function activateTag(tagItem: TagTreeItem | Tag): Promise { + const tag = tagItem instanceof TagTreeItem ? tagItem.tag : tagItem; await functionsClient?.updateTag(tag.functionId, tag.$id); } diff --git a/src/commands/functions/createExecution.ts b/src/commands/functions/createExecution.ts index 3c246da..76e299f 100644 --- a/src/commands/functions/createExecution.ts +++ b/src/commands/functions/createExecution.ts @@ -1,10 +1,57 @@ +import { window } from 'vscode'; +import { Execution } from '../../appwrite'; import { functionsClient } from '../../client'; import { ext } from '../../extensionVariables'; import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem'; +import { sleep } from '../../utils/sleep'; +import { viewExecutionErrors } from './viewExecutionErrors'; +import { viewExecutionOutput } from './viewExecutionOutput'; export async function createExecution(functionTreeItem: FunctionTreeItem): Promise { const func = functionTreeItem.func; - ext.outputChannel.appendLog(`Creating execution for function ${func.name}`); - - await functionsClient?.createExecution(func.$id); + await executeFunction(func.$id); +} + +export async function executeFunction(functionId: string): Promise { + ext.outputChannel.appendLog(`Creating execution for function with ID: ${functionId}`); + let execution = await functionsClient?.createExecution(functionId); + ext.outputChannel.appendLog(JSON.stringify(execution, null, 2)); + await ext.tree?.functions?.refresh(); + + if (execution === undefined) { + return; + } + + execution = await waitForExecution(execution); + ext.tree?.functions?.refresh(); + + if (execution === undefined) { + return; + } + + const failed = execution.status === "failed"; + const item = !failed ? "View output" : "View errors"; + const action = await window.showInformationMessage(`Execution ${failed ? "failed" : "completed"} in ${execution.time.toFixed(2)}s.`, item); + if (action === item) { + if (item === "View output") { + await viewExecutionOutput(execution); + return; + } + await viewExecutionErrors(execution); + return; + } +} + +async function waitForExecution(execution: Execution | undefined): Promise { + if (execution === undefined) { + return; + } + if (execution.status === "processing" || execution.status === "waiting") { + await sleep(5000); + + ext.outputChannel.appendLog("Execution still ..."); + return await waitForExecution(await functionsClient?.getExecution(execution.functionId, execution.$id)); + } + + return execution; } diff --git a/src/commands/functions/createTag.ts b/src/commands/functions/createTag.ts index e00c829..b154d33 100644 --- a/src/commands/functions/createTag.ts +++ b/src/commands/functions/createTag.ts @@ -1,41 +1,164 @@ -import { ProgressLocation, Uri, window } from "vscode"; -import { functionsClient, storageClient } from "../../client"; +import { ProgressLocation, QuickPickItem, Uri, window, workspace } from "vscode"; +import { functionsClient } 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 { +import { ProgressMessage } from "../../utils/types"; +import { Tag } from "../../appwrite"; +import { activateTag } from "./activateTag"; + +export async function createTag(item?: TagsTreeItem | Uri): Promise { if (item instanceof Uri) { - window.withProgress({ location: ProgressLocation.Notification, title: "Creating tag..." }, async (_progress, _token) => { - await createTagFromUri(item); - }); + const functions = await functionsClient?.list(); + if (functions === undefined) { + return; + } + const pick = await window.showQuickPick( + functions.functions.map( + (func): QuickPickItem => ({ label: func.name, description: func.env, detail: func.$id }) + ), + { placeHolder: "Select a function to create tag" } + ); + if (pick === undefined || pick.detail === undefined) { + return; + } + const tags = await functionsClient?.listTags(pick.detail); + let value; + if (tags && tags.tags.length > 0) { + value = tags.tags[tags.tags.length - 1].command; + } + const command = await window.showInputBox({ value, prompt: "Command to run your code" }); + if (command === undefined) { + return; + } + const tag = await window.withProgress( + { location: ProgressLocation.Notification, title: "Creating tag..." }, + async (progress, _token) => { + if (pick.detail === undefined) { + return; + } + return await createTagFromUri(pick.detail, command, item, progress); + } + ); + if (tag) { + await tagNotification(tag); + } + return; } if (item instanceof TagsTreeItem) { + const func = item.parent.func; 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)); - }); + if (folder === undefined || folder === "") { + return; + } + const tags = await functionsClient?.listTags(func.$id); + let value; + if (tags && tags.tags.length > 0) { + value = tags.tags[tags.tags.length - 1].command; + } + const command = await window.showInputBox({ value, prompt: "Command to run your code" }); + if (command === undefined) { + return; + } + const tag = await window.withProgress( + { location: ProgressLocation.Notification, title: "Creating tag..." }, + async (progress, _token) => { + return await createTagFromUri(func.$id, command, Uri.parse(folder), progress); + } + ); + + if (tag) { + await tagNotification(tag); + return; + } + } + + if (item === undefined) { + const functions = await functionsClient?.list(); + if (functions === undefined) { + return; + } + const pick = await window.showQuickPick( + functions.functions.map( + (func): QuickPickItem => ({ label: func.name, description: func.env, detail: func.$id }) + ), + { placeHolder: "Select a function to create tag" } + ); + if (pick === undefined || pick.detail === undefined) { + return; + } + const funcId = pick.detail; + const folder = await selectWorkspaceFolder("Select folder of your function code."); + const tags = await functionsClient?.listTags(funcId); + let value; + if (tags && tags.tags.length > 0) { + value = tags.tags[tags.tags.length - 1].command; + } + const command = await window.showInputBox({ value, prompt: "Command to run your code" }); + if (command === undefined) { + return; + } + const tag = await window.withProgress( + { location: ProgressLocation.Notification, title: "Creating tag..." }, + async (progress, _token) => { + return await createTagFromUri(funcId, command, Uri.parse(folder), progress); + } + ); + + if (tag) { + await tagNotification(tag); + return; + } } } -async function createTagFromUri(uri: Uri): Promise { - const tarFilePath = await getTarReadStream(uri); +async function createTagFromUri(functionId: string, command: string, uri: Uri, progress: ProgressMessage): Promise { + progress.report({ message: "Creating tarball", increment: 10 }); + if (functionsClient === undefined) { return; } - if (tarFilePath === undefined) { - ext.outputChannel.appendLog("Error creating tar file."); + let tarFilePath; + try { + tarFilePath = await getTarReadStream(uri); + } catch (e) { + window.showErrorMessage("Error creating tar file.\n" + e); return; } + if (tarFilePath === undefined) { + window.showErrorMessage("Failed to create tar file."); + ext.outputChannel.appendLog("Failed to create tar file."); + return; + } + // somehow makes the upload work + await workspace.fs.readFile(Uri.file(tarFilePath)); + progress.report({ message: "Uploading tag", increment: 60 }); try { - await functionsClient.createTag("60b1836a8e5d9", "python ./hello.py", fs.createReadStream(tarFilePath)); - await storageClient?.createFile(fs.createReadStream(tarFilePath)); + return await functionsClient.createTag(functionId, command, fs.createReadStream(tarFilePath)); } catch (e) { ext.outputChannel.appendLog("Creating tag error: " + e); } } + +async function tagNotification(tag: Tag): Promise { + ext.tree?.functions?.refresh(); + if (tag) { + const action = await window.showInformationMessage( + `Successfully created tag with size ${tag.size}B.`, + "Activate tag", + "View in console" + ); + if (action === "Activate tag") { + await activateTag(tag); + } + if (action === "View in console") { + // + } + return; + } +} diff --git a/src/commands/functions/viewExecutionErrors.ts b/src/commands/functions/viewExecutionErrors.ts index 3d5c154..2a79f28 100644 --- a/src/commands/functions/viewExecutionErrors.ts +++ b/src/commands/functions/viewExecutionErrors.ts @@ -1,11 +1,16 @@ +import { Execution } from '../../appwrite'; import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; import { openReadOnlyContent } from "../../ui/openReadonlyContent"; -export async function viewExecutionErrors(executionItem: ExecutionTreeItem): Promise { +export async function viewExecutionErrors(executionItem: ExecutionTreeItem | Execution): 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'); + let execution = executionItem as Execution; + + if (executionItem instanceof ExecutionTreeItem) { + execution = executionItem.execution; + } + await openReadOnlyContent({ label: `Execution stderr`, fullId: `${execution.$id}-errors.txt` }, execution.stderr, '.txt'); } diff --git a/src/commands/functions/viewExecutionOutput.ts b/src/commands/functions/viewExecutionOutput.ts index 94e1415..28088c5 100644 --- a/src/commands/functions/viewExecutionOutput.ts +++ b/src/commands/functions/viewExecutionOutput.ts @@ -1,11 +1,19 @@ +import { Execution } from '../../appwrite'; import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; import { openReadOnlyContent } from "../../ui/openReadonlyContent"; -export async function viewExecutionOutput(executionItem: ExecutionTreeItem): Promise { +export async function viewExecutionOutput(executionItem: ExecutionTreeItem | Execution): 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'); + + let execution = executionItem as Execution; + + if (executionItem instanceof ExecutionTreeItem) { + execution = executionItem.execution; + } + console.log(execution.dateCreated); + + await openReadOnlyContent({ label: `Execution stdout`, fullId: `${execution.$id}-output.txt` }, execution.stdout, '.txt'); } diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 5b6a27d..4bb1f94 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -42,6 +42,8 @@ import { openExecutionsInBrowser } from './functions/openExecutionsInBrowser'; import { openFunctionSettingsInBrowser } from './functions/openFunctionSettingsInBrowser'; import { openFunctionTagsInBrowser } from './functions/openFunctionTagsInBrowser'; +import { viewMore } from './common/viewMore'; + class CommandRegistrar { constructor(private readonly context: ExtensionContext) {} @@ -74,6 +76,7 @@ export function registerCommands(context: ExtensionContext): void { /** Common **/ registerCommand("editValue", editValue); + registerCommand("viewMore", viewMore); /** General **/ registerCommand("Connect", connectAppwrite, "all"); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index b6681ad..bacaaf7 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -17,7 +17,7 @@ export type AppwriteTree = { }; export type Ext = { - context?: ExtensionContext; + context: ExtensionContext; outputChannel: AppwriteOutputChannel; tree?: AppwriteTree; }; diff --git a/src/tree/common/editable/EditableTreeItemBase.ts b/src/tree/common/editable/EditableTreeItemBase.ts new file mode 100644 index 0000000..70bfd00 --- /dev/null +++ b/src/tree/common/editable/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/editable/EnumEditableTreeItem.ts b/src/tree/common/editable/EnumEditableTreeItem.ts new file mode 100644 index 0000000..9969f9b --- /dev/null +++ b/src/tree/common/editable/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/editable/SimpleEditableTreeItem.ts b/src/tree/common/editable/SimpleEditableTreeItem.ts new file mode 100644 index 0000000..834e299 --- /dev/null +++ b/src/tree/common/editable/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/editable/StringEditableTreeItem.ts b/src/tree/common/editable/StringEditableTreeItem.ts new file mode 100644 index 0000000..e4f77b0 --- /dev/null +++ b/src/tree/common/editable/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/database/DatabaseTreeItemProvider.ts b/src/tree/database/DatabaseTreeItemProvider.ts index d5a9b26..9d92c16 100644 --- a/src/tree/database/DatabaseTreeItemProvider.ts +++ b/src/tree/database/DatabaseTreeItemProvider.ts @@ -4,8 +4,8 @@ import AppwriteCall from "../../utils/AppwriteCall"; import { Collection, CollectionsList } from "../../appwrite"; import { CollectionTreeItem } from "./CollectionTreeItem"; import { AppwriteSDK } from "../../constants"; -import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; import { ext } from '../../extensionVariables'; +import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; export class DatabaseTreeItemProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< @@ -34,7 +34,7 @@ export class DatabaseTreeItemProvider implements vscode.TreeDataProvider = { 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) { + constructor(public readonly parent: ExecutionsTreeItem, public execution: Execution) { super(execution.$id); this.label = this.getLabel(execution); this.iconPath = executionStatusIcons[execution.status]; @@ -31,40 +23,45 @@ export class ExecutionTreeItem extends TreeItem { this.description = execution.trigger; this.contextValue = this.getContextValue(execution); this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting"; - this.autoRefresh(); + // if (this.isAutoRefreshing) { + // this.autoRefresh(); + // } } - async autoRefresh(): Promise { - if (!this.isAutoRefreshing) { - return; - } - this.refreshCount++; - ext.outputChannel.appendLog("Refreshing execution."); - const execution = await functionsClient?.getExecution(this.parent.parent.func.$id, this.execution.$id); + // async autoRefresh(): Promise { + // if (!this.isAutoRefreshing && this.refreshCount < 5) { + // 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(); - } + // if (!execution) { + // ext.outputChannel.appendLog("Execution is undefined"); + // this.isAutoRefreshing = false; + // return; + // } + // this.execution = execution; + // 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)} (${this.getExecutionTime(execution)}s)`; } return `${this.getCreated(execution)} (${execution.status})`; } + getExecutionTime(execution: Execution): string { + return execution.time.toPrecision(2); + } + getContextValue(execution: Execution): string { if (execution.status === "completed" || execution.status === "failed") { if (execution.stderr === "" && execution.stdout === "") { diff --git a/src/tree/functions/executions/ExecutionsTreeItem.ts b/src/tree/functions/executions/ExecutionsTreeItem.ts index 916d2e1..148409b 100644 --- a/src/tree/functions/executions/ExecutionsTreeItem.ts +++ b/src/tree/functions/executions/ExecutionsTreeItem.ts @@ -1,21 +1,47 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; -import { Execution, ExecutionList } from '../../../appwrite'; +import { Execution, ExecutionList } from "../../../appwrite"; import { functionsClient } from "../../../client"; -import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; -import { ExecutionTreeItem } from './ExecutionTreeItem'; -import { FunctionTreeItem } from '../FunctionTreeItem'; +import { ExecutionTreeItem } from "./ExecutionTreeItem"; +import { FunctionTreeItem } from "../FunctionTreeItem"; +import { ext } from "../../../extensionVariables"; +import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; export class ExecutionsTreeItem extends AppwriteTreeItemBase { constructor(public readonly parent: FunctionTreeItem) { super(parent, "Executions"); } + private executionsToShow = 10; + public async getChildren(): Promise { 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.')]; + const executions: ExecutionList | undefined = await functionsClient.listExecutions( + this.parent.func.$id, + undefined, + this.executionsToShow, + undefined, + "DESC" + ); + const children = executions?.executions.map((execution: Execution) => new ExecutionTreeItem(this, execution)) ?? [ + new TreeItem("No executions."), + ]; + if (children.length === 0) { + children.push(new TreeItem("No executions.")); + } + ext.outputChannel.appendLog(`Found ${executions?.sum} executions`); + if (executions?.sum ?? (0 > this.executionsToShow && this.executionsToShow !== 100)) { + const viewMoreItem: TreeItem = { + command: { + command: "vscode-appwrite.viewMore", + arguments: [this], + title: "View more", + }, + label: "View more...", + }; + children.push(viewMoreItem); + } return children; } @@ -24,4 +50,12 @@ export class ExecutionsTreeItem extends AppwriteTreeItemBase { contextValue = "executions"; iconPath = new ThemeIcon("history"); + + async viewMore(): Promise { + this.executionsToShow += 10; + if (this.executionsToShow > 100) { + this.executionsToShow = 100; + } + ext.tree?.functions?.refreshChild(this); + } } diff --git a/src/tree/functions/settings/EventsTreeItem.ts b/src/tree/functions/settings/EventsTreeItem.ts index 2346b13..89bf252 100644 --- a/src/tree/functions/settings/EventsTreeItem.ts +++ b/src/tree/functions/settings/EventsTreeItem.ts @@ -3,7 +3,7 @@ import { Function } from "../../../appwrite"; import { functionsClient } from "../../../client"; import { appwriteSystemEvents } from "../../../constants"; import { ext } from "../../../extensionVariables"; -import { EnumEditableTreeItemBase } from "../../common/EnumEditableTreeItem"; +import { EnumEditableTreeItemBase } from "../../common/editable/EnumEditableTreeItem"; import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; export class EventsTreeItem extends EnumEditableTreeItemBase { diff --git a/src/tree/functions/settings/FunctionSettingsTreeItem.ts b/src/tree/functions/settings/FunctionSettingsTreeItem.ts index 941c2a9..654ac2a 100644 --- a/src/tree/functions/settings/FunctionSettingsTreeItem.ts +++ b/src/tree/functions/settings/FunctionSettingsTreeItem.ts @@ -1,7 +1,7 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; import { Function } from "../../../appwrite"; import { functionsClient } from "../../../client"; -import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; +import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; import { ChildTreeItem } from "../../ChildTreeItem"; import { FunctionTreeItem } from "../FunctionTreeItem"; import { EventsTreeItem } from "./EventsTreeItem"; diff --git a/src/tree/functions/settings/NameTreeItem.ts b/src/tree/functions/settings/NameTreeItem.ts index 159fd0f..25f6c30 100644 --- a/src/tree/functions/settings/NameTreeItem.ts +++ b/src/tree/functions/settings/NameTreeItem.ts @@ -2,7 +2,7 @@ import { InputBoxOptions, MarkdownString } from "vscode"; import { Function } from "../../../appwrite"; import { functionsClient } from "../../../client"; import { ext } from "../../../extensionVariables"; -import { StringEditableTreeItemBase } from '../../common/StringEditableTreeItem'; +import { StringEditableTreeItemBase } from '../../common/editable/StringEditableTreeItem'; import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; const tooltip = "Function name"; diff --git a/src/tree/functions/settings/ScheduleTreeItem.ts b/src/tree/functions/settings/ScheduleTreeItem.ts index de2e3bb..10a2e15 100644 --- a/src/tree/functions/settings/ScheduleTreeItem.ts +++ b/src/tree/functions/settings/ScheduleTreeItem.ts @@ -5,7 +5,7 @@ import { ext } from "../../../extensionVariables"; import cron from "cron-validate"; import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; import cronstrue from "cronstrue"; -import { StringEditableTreeItemBase } from '../../common/StringEditableTreeItem'; +import { StringEditableTreeItemBase } from '../../common/editable/StringEditableTreeItem'; export class ScheduleTreeItem extends StringEditableTreeItemBase { private readonly func: Function; diff --git a/src/tree/functions/settings/TimeoutTreeItem.ts b/src/tree/functions/settings/TimeoutTreeItem.ts index 3b5da4c..b2e8a62 100644 --- a/src/tree/functions/settings/TimeoutTreeItem.ts +++ b/src/tree/functions/settings/TimeoutTreeItem.ts @@ -2,7 +2,7 @@ import { InputBoxOptions, MarkdownString } from "vscode"; import { Function } from "../../../appwrite"; import { functionsClient } from "../../../client"; import { ext } from "../../../extensionVariables"; -import { StringEditableTreeItemBase } from "../../common/StringEditableTreeItem"; +import { StringEditableTreeItemBase } from "../../common/editable/StringEditableTreeItem"; function isNumeric(str: string) { console.log("here"); diff --git a/src/tree/functions/settings/VarTreeItem.ts b/src/tree/functions/settings/VarTreeItem.ts index c1134b8..cd64572 100644 --- a/src/tree/functions/settings/VarTreeItem.ts +++ b/src/tree/functions/settings/VarTreeItem.ts @@ -2,7 +2,7 @@ import { InputBoxOptions, MarkdownString, window } from "vscode"; import { Function } from "../../../appwrite"; import { functionsClient } from "../../../client"; import { ext } from "../../../extensionVariables"; -import { StringEditableTreeItemBase } from "../../common/StringEditableTreeItem"; +import { StringEditableTreeItemBase } from "../../common/editable/StringEditableTreeItem"; import { VarsTreeItem } from "./VarsTreeItem"; const tooltip = "Environment var"; diff --git a/src/tree/functions/settings/VarsTreeItem.ts b/src/tree/functions/settings/VarsTreeItem.ts index e66496b..165b8dc 100644 --- a/src/tree/functions/settings/VarsTreeItem.ts +++ b/src/tree/functions/settings/VarsTreeItem.ts @@ -1,6 +1,6 @@ import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { Vars } from "../../../appwrite"; -import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; +import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; import { VarTreeItem } from "./VarTreeItem"; diff --git a/src/tree/functions/tags/TagTreeItem.ts b/src/tree/functions/tags/TagTreeItem.ts index 2a468fa..b34208a 100644 --- a/src/tree/functions/tags/TagTreeItem.ts +++ b/src/tree/functions/tags/TagTreeItem.ts @@ -10,7 +10,7 @@ export class TagTreeItem extends TreeItem { 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.iconPath = new ThemeIcon(active ? 'circle-large-filled' : 'circle-large-outline'); this.contextValue = `tag${active ? '_active' : ''}`; this.tooltip = new MarkdownString(`ID: ${tag.$id} \nCreated: ${msToDate(tag.dateCreated)} \nCommand: ${tag.command} \nSize: ${tag.size}B`); } diff --git a/src/tree/functions/tags/TagsTreeItem.ts b/src/tree/functions/tags/TagsTreeItem.ts index 3f65829..e9f7f4a 100644 --- a/src/tree/functions/tags/TagsTreeItem.ts +++ b/src/tree/functions/tags/TagsTreeItem.ts @@ -14,7 +14,22 @@ export class TagsTreeItem extends AppwriteTreeItemBase { 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.')]; + const children = tags?.tags.sort((a, b) => b.dateCreated - a.dateCreated).map((tag) => new TagTreeItem(this, tag)) ?? [new TreeItem('No tags.')]; + + if (children.length === 0) { + const noTagsItem: TreeItem = { + command: { + command: "vscode-appwrite.CreateTag", + title: "Create tag", + arguments: [this], + tooltip: "Create a tag" + }, + label: "Create a tag", + iconPath: new ThemeIcon("cloud-upload"), + }; + children.push(noTagsItem); + } + return children; } collapsibleState = TreeItemCollapsibleState.Collapsed; diff --git a/src/ui/AppwriteTreeItemBase.ts b/src/ui/AppwriteTreeItemBase.ts index 00fd410..a38ead9 100644 --- a/src/ui/AppwriteTreeItemBase.ts +++ b/src/ui/AppwriteTreeItemBase.ts @@ -7,4 +7,7 @@ export abstract class AppwriteTreeItemBase extends TreeItem { abstract getChildren?(): Promise; + viewMore(): Promise { + return Promise.resolve(); + } } diff --git a/src/utils/date.ts b/src/utils/date.ts index f5da0cc..adad317 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,5 +1,9 @@ -import dayjs = require('dayjs'); +import dayjs = require("dayjs"); +import utc = require("dayjs/plugin/utc"); +import timezone = require("dayjs/plugin/timezone"); // dependent on utc plugin +dayjs.extend(utc); +dayjs.extend(timezone); export function msToDate(ms: number): string { - return dayjs(ms).format("LTS"); + return dayjs(ms * 1000).tz(dayjs.tz.guess()).format("LTS"); } diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 0000000..2b5e993 --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/src/utils/tar.ts b/src/utils/tar.ts index 8f81821..f8a8317 100644 --- a/src/utils/tar.ts +++ b/src/utils/tar.ts @@ -2,8 +2,8 @@ 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 { sleep } from './sleep'; export async function getTarReadStream(folder: Uri): Promise { try { @@ -15,11 +15,14 @@ export async function getTarReadStream(folder: Uri): Promise window.showErrorMessage("No workspace open."); return; } + ext.outputChannel.appendLog(`Creating '${tarName}' in '${workspace.workspaceFolders?.[0].uri.fsPath}'...`); + const tarFilePath = path.join(ext.context?.globalStorageUri.fsPath ?? '', tarName); + await workspace.fs.createDirectory(ext.context.globalStorageUri); - const tarFilePath = path.join(os.tmpdir(), tarName); + tar.create({ gzip: true, cwd: cwd, mode: 1777 }, [path.relative(cwd, folder.fsPath)]).pipe(fs.createWriteStream(tarFilePath)); - tar.create({ gzip: true, cwd: cwd }, [path.relative(cwd, folder.fsPath)]).pipe(fs.createWriteStream(tarFilePath)); + await sleep(500); ext.outputChannel.appendLog(`Created ${tarFilePath}`); diff --git a/src/utils/types.d.ts b/src/utils/types.d.ts new file mode 100644 index 0000000..9ab797c --- /dev/null +++ b/src/utils/types.d.ts @@ -0,0 +1,6 @@ +import { Progress } from 'vscode'; + +export type ProgressMessage = Progress<{ + message?: string | undefined; + increment?: number | undefined; +}>;