diff --git a/README.md b/README.md index eec00f6..d5795e1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ From [appwrite.io](https://appwrite.io) ## Features +### Connect to multiple Appwrite projects + +![Mutliple projects](media/features/projects/projectsView1.gif) + ### View database documents right inside VS Code. ![Database feature](media/features/database/scr2.png) diff --git a/media/features/projects/projectsView1.gif b/media/features/projects/projectsView1.gif new file mode 100644 index 0000000..9982230 Binary files /dev/null and b/media/features/projects/projectsView1.gif differ diff --git a/package.json b/package.json index 67c586b..f41f620 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,25 @@ "command": "vscode-appwrite.openStorageDocumentation", "title": "Open storage documentation", "icon": "$(book)" + }, + { + "command": "vscode-appwrite.addProject", + "title": "Add Appwrite project", + "icon": "$(plus)" + }, + { + "command": "vscode-appwrite.setActiveProject", + "title": "Set as active" + }, + { + "command": "vscode-appwrite.refreshProjects", + "title": "Refresh projects", + "icon": "$(refresh)" + }, + { + "command": "vscode-appwrite.removeProject", + "title": "Remove project", + "icon": "$(trash)" } ], "views": { @@ -188,6 +207,10 @@ { "id": "Health", "name": "Health" + }, + { + "id": "Projects", + "name": "Projects" } ] }, @@ -195,6 +218,10 @@ { "view": "Users", "contents": "Connect to Appwrite to get started.\n[Connect to Appwrite](command:vscode-appwrite.Connect)" + }, + { + "view": "Projects", + "contents": "Add an Appwrite project to get started.\n[Connect to Appwrite](command:vscode-appwrite.Connect)" } ], "menus": { @@ -248,6 +275,16 @@ "command": "vscode-appwrite.openStorageDocumentation", "when": "view == Storage", "group": "navigation" + }, + { + "command": "vscode-appwrite.refreshProjects", + "when": "view == Projects", + "group": "navigation" + }, + { + "command": "vscode-appwrite.addProject", + "when": "view == Projects", + "group": "navigation" } ], "view/item/context": [ @@ -337,6 +374,15 @@ "command": "vscode-appwrite.deletePermission", "when": "viewItem =~ /^(permission)$/", "group": "inline" + }, + { + "command": "vscode-appwrite.setActiveProject", + "when": "viewItem =~ /^(appwriteProject)$/", + "group": "inline" + }, + { + "command": "vscode-appwrite.removeProject", + "when": "viewItem =~ /(appwriteProject)/" } ], "commandPalette": [ @@ -428,6 +474,11 @@ "type": "array", "default": [], "markdownDescription": "List of Appwrite project configurations. You can use the Connect command to set this up, or see [docs](https://github.com/streamlux/vscode-appwrite/) for more information." + }, + "appwrite.activeProjectId": { + "type": "string", + "default": "", + "markdownDescription": "Project id of the active project, see [docs](https://github.com/streamlux/vscode-appwrite/) for more information." } } } diff --git a/src/commands/project/removeProject.ts b/src/commands/project/removeProject.ts new file mode 100644 index 0000000..77c875e --- /dev/null +++ b/src/commands/project/removeProject.ts @@ -0,0 +1,14 @@ +import { window } from "vscode"; +import { initAppwriteClient } from "../../client"; +import { removeProjectConfig } from '../../settings'; +import { ProjectTreeItem } from '../../tree/projects/ProjectTreeItem'; +import { addProjectWizard } from "../../ui/AddProjectWizard"; + +export async function removeProject(project: ProjectTreeItem | string) { + if (typeof project === 'string') { + await removeProjectConfig(project); + return; + } + + await removeProjectConfig(project.project.projectId); +} diff --git a/src/commands/project/setActiveProject.ts b/src/commands/project/setActiveProject.ts new file mode 100644 index 0000000..fcf4d76 --- /dev/null +++ b/src/commands/project/setActiveProject.ts @@ -0,0 +1,16 @@ +import { window } from "vscode"; +import { initAppwriteClient } from "../../client"; +import { setActiveProjectId } from '../../settings'; +import { ProjectTreeItem } from "../../tree/projects/ProjectTreeItem"; + +export async function setActiveProject(treeItem: ProjectTreeItem) { + if (treeItem === undefined) { + return; + } + + if (!(treeItem instanceof ProjectTreeItem)) { + return; + } + + await setActiveProjectId(treeItem.project.projectId); +} diff --git a/src/commands/project/switchProject.ts b/src/commands/project/switchProject.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index fad28d5..462cc85 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -1,6 +1,6 @@ import { commands, ExtensionContext } from "vscode"; import { AppwriteTree, ext } from "../extensionVariables"; -import { refreshTree } from "../utils/refreshTree"; +import { refreshAllViews, refreshTree } from "../utils/refreshTree"; import { connectAppwrite } from "./connectAppwrite"; import { createCollection } from "./database/createCollection"; import { createPermission } from "./database/permissions/createPermission"; @@ -14,7 +14,6 @@ import { refreshCollectionsList } from "./database/refreshCollectionsList"; import { removeRule } from "./database/removeRule"; import { viewCollectionAsJson } from "./database/viewCollectionAsJson"; import { openDocumentation } from "./openDocumentation"; -import { addProject } from "./project/addProject"; import { copyUserEmail } from "./users/copyUserEmail"; import { copyUserId } from "./users/copyUserId"; import { createUser } from "./users/createUser"; @@ -24,6 +23,8 @@ import { openUserInConsole } from "./users/openUserInConsole"; import { refreshUsersList } from "./users/refreshUsersList"; import { viewUserPrefs } from "./users/viewUserPrefs"; import { editPermission } from "./database/permissions/editPermission"; +import { setActiveProject } from "./project/setActiveProject"; +import { removeProject } from './project/removeProject'; class CommandRegistrar { constructor(private readonly context: ExtensionContext) {} @@ -36,11 +37,21 @@ class CommandRegistrar { export function registerCommands(context: ExtensionContext): void { const registrar = new CommandRegistrar(context); - const registerCommand = (commandId: string, callback: (...args: any[]) => any, refresh?: keyof AppwriteTree) => { + const registerCommand = ( + commandId: string, + callback?: (...args: any[]) => any, + refresh?: keyof AppwriteTree | (keyof AppwriteTree)[] | "all" + ) => { registrar.registerCommand(`vscode-appwrite.${commandId}`, async (...args: any[]) => { - await callback(...args); - if (refresh) { - refreshTree(refresh); + await callback?.(...args); + if (refresh !== undefined) { + if (refresh === "all") { + refreshAllViews(); + } else if (typeof refresh === "string") { + refreshTree(refresh); + } else { + refreshTree(...refresh); + } } }); }; @@ -81,4 +92,10 @@ export function registerCommands(context: ExtensionContext): void { /** Storage **/ registerCommand("refreshStorage", () => {}, "storage"); registerCommand("openStorageDocumentation", () => openDocumentation("storage")); + + /** Projects **/ + registerCommand("addProject", connectAppwrite, "all"); + registerCommand("setActiveProject", setActiveProject, "all"); + registerCommand("refreshProjects", undefined, "projects"); + registerCommand("removeProject", removeProject, "all"); } diff --git a/src/extension.ts b/src/extension.ts index fc78c3e..3bd7da2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,12 @@ import * as vscode from "vscode"; +import { workspace } from 'vscode'; import { initAppwriteClient } from "./client"; import { registerCommands } from "./commands/registerCommands"; import { ext } from "./extensionVariables"; -import { getDefaultProject } from "./settings"; +import { getActiveProjectConfiguration, getActiveProjectId, getDefaultProject } from "./settings"; import { DatabaseTreeItemProvider } from "./tree/database/DatabaseTreeItemProvider"; import { HealthTreeItemProvider } from "./tree/health/HealthTreeItemProvider"; +import { ProjectsTreeItemProvider } from './tree/projects/ProjectsTreeItemProvider'; import { StorageTreeItemProvider } from "./tree/storage/StorageTreeItemProvider"; import { UserTreeItemProvider } from "./tree/users/UserTreeItemProvider"; import { createAppwriteOutputChannel } from "./ui/AppwriteOutputChannel"; @@ -14,15 +16,17 @@ export async function activate(context: vscode.ExtensionContext): Promise const healthTreeItemProvider = new HealthTreeItemProvider(); const databaseTreeItemProvider = new DatabaseTreeItemProvider(); const storageTreeItemProvider = new StorageTreeItemProvider(); + const projectsTreeItemProvider = new ProjectsTreeItemProvider(); 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); - const defaultProject = await getDefaultProject(); - if (defaultProject) { - initAppwriteClient(defaultProject); + const activeProject = await getActiveProjectConfiguration(); + if (activeProject) { + initAppwriteClient(activeProject); } ext.context = context; @@ -33,6 +37,7 @@ export async function activate(context: vscode.ExtensionContext): Promise health: healthTreeItemProvider, database: databaseTreeItemProvider, storage: storageTreeItemProvider, + projects: projectsTreeItemProvider }; registerCommands(context); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 596d0c1..f647c09 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -1,6 +1,7 @@ import { ExtensionContext, OutputChannel } from "vscode"; import { DatabaseTreeItemProvider } from './tree/database/DatabaseTreeItemProvider'; import { HealthTreeItemProvider } from './tree/health/HealthTreeItemProvider'; +import { ProjectsTreeItemProvider } from './tree/projects/ProjectsTreeItemProvider'; import { StorageTreeItemProvider } from './tree/storage/StorageTreeItemProvider'; import { UserTreeItemProvider } from './tree/users/UserTreeItemProvider'; import { AppwriteOutputChannel } from './ui/AppwriteOutputChannel'; @@ -10,6 +11,7 @@ export type AppwriteTree = { health?: HealthTreeItemProvider; database?: DatabaseTreeItemProvider; storage?: StorageTreeItemProvider; + projects?: ProjectsTreeItemProvider; }; export type Ext = { diff --git a/src/settings.ts b/src/settings.ts index 0b2b190..747d60a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,5 @@ -import { workspace } from 'vscode'; +import { workspace } from "vscode"; +import { initAppwriteClient } from "./client"; export type AppwriteProjectConfiguration = { nickname?: string; @@ -14,17 +15,68 @@ export async function getDefaultProject(): Promise { - const configuration = workspace.getConfiguration('appwrite'); - const projects = configuration.get('projects'); + const configuration = workspace.getConfiguration("appwrite"); + const projects = configuration.get("projects"); if (projects === undefined) { - configuration.update('projects', []); + configuration.update("projects", []); return []; } return projects as AppwriteProjectConfiguration[]; } export async function addProjectConfiguration(projectConfig: AppwriteProjectConfiguration): Promise { - const configuration = workspace.getConfiguration('appwrite'); + const configuration = workspace.getConfiguration("appwrite"); const projects = await getAppwriteProjects(); - await configuration.update('projects', [...projects, projectConfig], true); + + await configuration.update("projects", [...projects, projectConfig], true); + await setActiveProjectId(projectConfig.projectId); +} + +export async function getActiveProjectId(): Promise { + const configuration = workspace.getConfiguration("appwrite"); + const projectId = configuration.get("activeProjectId"); + return projectId ?? ""; +} + +export async function getActiveProjectConfiguration(): Promise { + const configurations = await getAppwriteProjects(); + const activeConfigId = await getActiveProjectId(); + let activeConfig; + configurations.forEach((config) => { + if (config.projectId === activeConfigId) { + activeConfig = config; + } + }); + + if (activeConfig === undefined) { + activeConfig = configurations[0]; + setActiveProjectId(configurations[0].projectId); + } + return activeConfig; +} + +export async function setActiveProjectId(projectId: string): Promise { + const configuration = workspace.getConfiguration("appwrite"); + await configuration.update("activeProjectId", projectId, true); + initAppwriteClient(await getActiveProjectConfiguration()); +} + +export async function updateActiveProjectId(): Promise { + const projects = await getAppwriteProjects(); + if (projects.length > 0) { + const configuration = workspace.getConfiguration("appwrite"); + await configuration.update("activeProjectId", projects[0].projectId, true); + initAppwriteClient(await getActiveProjectConfiguration()); + } +} + +export async function removeProjectConfig(projectId: string): Promise { + const projects = await getAppwriteProjects(); + + const activeProjectId = await getActiveProjectId(); + + const updatedProjects = projects.filter((project) => project.projectId !== projectId); + const configuration = workspace.getConfiguration("appwrite"); + await configuration.update("projects", updatedProjects, true); + await updateActiveProjectId(); } diff --git a/src/tree/projects/ProjectTreeItem.ts b/src/tree/projects/ProjectTreeItem.ts new file mode 100644 index 0000000..1ea1976 --- /dev/null +++ b/src/tree/projects/ProjectTreeItem.ts @@ -0,0 +1,15 @@ +import { ThemeIcon, TreeItem } from "vscode"; +import { AppwriteProjectConfiguration } from "../../settings"; + +export class ProjectTreeItem extends TreeItem { + constructor(public readonly project: AppwriteProjectConfiguration, active: boolean) { + super("Project"); + this.iconPath = new ThemeIcon("rocket"); + const name = project.nickname ?? "Project"; + this.label = `${name} ${active ? "(Active)" : ""}`; + this.contextValue = `appwriteProject${active ? "_active" : ""}`; + if (!active) { + this.command = { command: "vscode-appwrite.setActiveProject", title: "Set active", arguments: [this] }; + } + } +} diff --git a/src/tree/projects/ProjectsTreeItemProvider.ts b/src/tree/projects/ProjectsTreeItemProvider.ts new file mode 100644 index 0000000..1e4ff91 --- /dev/null +++ b/src/tree/projects/ProjectsTreeItemProvider.ts @@ -0,0 +1,30 @@ +import * as vscode from "vscode"; +import { getActiveProjectId, getAppwriteProjects } from '../../settings'; +import { ProjectTreeItem } from './ProjectTreeItem'; + +export class ProjectsTreeItemProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< + vscode.TreeItem | undefined | void + >(); + + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor() {} + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + return element; + } + + async getChildren(element?: vscode.TreeItem): Promise { + const configs = await getAppwriteProjects(); + if (configs === undefined || configs.length === 0) { + return []; + } + const activeProjectId = await getActiveProjectId(); + return configs.map((config) => new ProjectTreeItem(config, config.projectId === activeProjectId)); + } +} diff --git a/src/tree/storage/StorageTreeItemProvider.ts b/src/tree/storage/StorageTreeItemProvider.ts index 0a9df0b..9e2875f 100644 --- a/src/tree/storage/StorageTreeItemProvider.ts +++ b/src/tree/storage/StorageTreeItemProvider.ts @@ -21,8 +21,9 @@ export class StorageTreeItemProvider implements vscode.TreeDataProvider { const files = await storageClient.listFiles(); - if (files === undefined) { - return []; + if (files === undefined || files?.files.length === 0) { + const noStorage = new vscode.TreeItem('No files found'); + return [noStorage]; } return files.files.map((file) => new FileTreeItem(file)); } diff --git a/src/ui/AddProjectWizard.ts b/src/ui/AddProjectWizard.ts index 41ac311..8b36ed5 100644 --- a/src/ui/AddProjectWizard.ts +++ b/src/ui/AddProjectWizard.ts @@ -7,18 +7,27 @@ export async function addProjectWizard(): Promise { ext.tree?.[tree]?.refresh(); - return; - } + }); +} +export function refreshAllViews(): void { if (ext.tree) { - Object.values(ext.tree).forEach((treeView) => { - treeView?.refresh(); + Object.keys(ext.tree).forEach((tree) => { + refreshTree(tree as keyof AppwriteTree); }); } }