From 116c65dc0e0f78ed3c7e78e2d5f8eeb961c616d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3jcik?= <58668583+Adam-it@users.noreply.github.com> Date: Fri, 15 Mar 2024 01:10:12 +0100 Subject: [PATCH] Enhance account and app catalog tree view. Closes: #127 (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎯 Aim Is to add more info about tenant and tenant wide extensions ## ✅ What was done - [X] refactored account view to have link to api management - [X] added tenant health incident list - [X] added tenant wide extension list ## 🔗 Related issue Closes: #127 #127 --- assets/fonts/fabric-icons.woff | Bin 0 -> 3264 bytes assets/icons/dark/M365.svg | 3 - assets/icons/dark/sharepoint.svg | 4 - assets/icons/light/M365.svg | 3 - assets/icons/light/sharepoint.svg | 4 - package.json | 72 +++++++++- src/constants/Commands.ts | 4 + src/panels/CommandPanel.ts | 124 +++++++++++------- ...wProvider.ts => ActionTreeDataProvider.ts} | 22 ++-- src/services/CliActions.ts | 45 ++++++- 10 files changed, 198 insertions(+), 83 deletions(-) create mode 100644 assets/fonts/fabric-icons.woff delete mode 100644 assets/icons/dark/M365.svg delete mode 100644 assets/icons/dark/sharepoint.svg delete mode 100644 assets/icons/light/M365.svg delete mode 100644 assets/icons/light/sharepoint.svg rename src/providers/{ActionTreeviewProvider.ts => ActionTreeDataProvider.ts} (64%) diff --git a/assets/fonts/fabric-icons.woff b/assets/fonts/fabric-icons.woff new file mode 100644 index 0000000000000000000000000000000000000000..386e4a921cde8a341d90c6bc74e9cadc92242f9b GIT binary patch literal 3264 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 literal 0 HcmV?d00001 diff --git a/assets/icons/dark/M365.svg b/assets/icons/dark/M365.svg deleted file mode 100644 index f1f8ece..0000000 --- a/assets/icons/dark/M365.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/dark/sharepoint.svg b/assets/icons/dark/sharepoint.svg deleted file mode 100644 index b4dbf40..0000000 --- a/assets/icons/dark/sharepoint.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/assets/icons/light/M365.svg b/assets/icons/light/M365.svg deleted file mode 100644 index ce9b3ff..0000000 --- a/assets/icons/light/M365.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/light/sharepoint.svg b/assets/icons/light/sharepoint.svg deleted file mode 100644 index 704bd78..0000000 --- a/assets/icons/light/sharepoint.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/package.json b/package.json index 465b11d..27cbd74 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,50 @@ "license": "MIT", "main": "./dist/extension.js", "contributes": { + "icons": { + "spo-m365": { + "description": "Microsoft 365 icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\ED72" + } + }, + "spo-logo": { + "description": "SharePoint Online icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\ED18" + } + }, + "spo-app-list": { + "description": "Tenant-Wide extension list icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\E31E" + } + }, + "spo-app": { + "description": "App icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\ECDC" + } + }, + "m365-warning": { + "description": "Warning icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\E7BA" + } + }, + "m365-health": { + "description": "Health icon", + "default": { + "fontPath": "./assets/fonts/fabric-icons.woff", + "fontCharacter": "\\E95E" + } + } + }, "viewsContainers": { "activitybar": [ { @@ -68,7 +112,7 @@ }, { "id": "pnp-view-environment", - "name": "Environment details", + "name": "App Catalog details", "when": "pnp.project.isSPFxProject && pnp.project.isLoggedIn" }, { @@ -166,6 +210,18 @@ "title": "Sign out from M365", "category": "Viva Connections Toolkit", "icon": "$(sign-out)" + }, + { + "command": "viva-connections-toolkit.refreshAppCatalogTreeView", + "title": "Refresh App Catalog list", + "category": "Viva Connections Toolkit", + "icon": "$(refresh)" + }, + { + "command": "viva-connections-toolkit.refreshAccountTreeView", + "title": "Refresh Account view", + "category": "Viva Connections Toolkit", + "icon": "$(refresh)" } ], "menus": { @@ -187,6 +243,18 @@ "when": "pnp.project.isSPFxProject" } ], + "view/title": [ + { + "command": "viva-connections-toolkit.refreshAppCatalogTreeView", + "when": "view == pnp-view-environment", + "group": "navigation" + }, + { + "command": "viva-connections-toolkit.refreshAccountTreeView", + "when": "view == pnp-view-account", + "group": "navigation" + } + ], "view/item/context": [ { "command": "viva-connections-toolkit.logout", @@ -276,4 +344,4 @@ "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0" } -} +} \ No newline at end of file diff --git a/src/constants/Commands.ts b/src/constants/Commands.ts index f0124bf..af22174 100644 --- a/src/constants/Commands.ts +++ b/src/constants/Commands.ts @@ -43,4 +43,8 @@ export const Commands = { // Serving serveProject: `${EXTENSION_NAME}.serveProject`, + + // TreeViews + refreshAppCatalogTreeView: `${EXTENSION_NAME}.refreshAppCatalogTreeView`, + refreshAccountTreeView: `${EXTENSION_NAME}.refreshAccountTreeView`, }; \ No newline at end of file diff --git a/src/panels/CommandPanel.ts b/src/panels/CommandPanel.ts index f00da12..f7f20b8 100644 --- a/src/panels/CommandPanel.ts +++ b/src/panels/CommandPanel.ts @@ -1,25 +1,32 @@ import { readFileSync } from 'fs'; import { commands, workspace, window, Uri } from 'vscode'; import { Commands, ContextKeys } from '../constants'; -import { ActionTreeItem, ActionTreeviewProvider } from '../providers/ActionTreeviewProvider'; +import { ActionTreeItem, ActionTreeDataProvider } from '../providers/ActionTreeDataProvider'; import { AuthProvider, M365AuthenticationSession } from '../providers/AuthProvider'; import { CliActions } from '../services/CliActions'; import { DebuggerCheck } from '../services/DebuggerCheck'; import { EnvironmentInformation } from '../services/EnvironmentInformation'; import { TeamsToolkitIntegration } from '../services/TeamsToolkitIntegration'; import { AdaptiveCardCheck } from '../services/AdaptiveCardCheck'; +import { Subscription } from '../models'; +import { Extension } from '../services/Extension'; export class CommandPanel { public static register() { + const subscriptions: Subscription[] = Extension.getInstance().subscriptions; + + subscriptions.push( + commands.registerCommand(Commands.refreshAppCatalogTreeView, CommandPanel.refreshEnvironmentTreeView) + ); + subscriptions.push( + commands.registerCommand(Commands.refreshAccountTreeView, CommandPanel.refreshAccountTreeView) + ); + CommandPanel.init(); } - /** - * Initialize the command panel - * @returns - */ private static async init() { let isTeamsToolkitProject = false; let files = await workspace.findFiles('.yo-rc.json', '**/node_modules/**'); @@ -52,13 +59,11 @@ export class CommandPanel { TeamsToolkitIntegration.isTeamsToolkitProject = isTeamsToolkitProject; + AdaptiveCardCheck.validateACEComponent(); CommandPanel.registerTreeView(); AuthProvider.verify(); } - /** - * Register all the treeviews - */ private static registerTreeView() { const authInstance = AuthProvider.getInstance(); if (authInstance) { @@ -78,20 +83,50 @@ export class CommandPanel { CommandPanel.helpTreeView(); } - /** - * Provide the actions for the account treeview - * @param session - */ - private static accountTreeView(session: M365AuthenticationSession | undefined) { + private static refreshAccountTreeView(){ + const authInstance = AuthProvider.getInstance(); + if (authInstance) { + authInstance.getAccount().then(account => CommandPanel.accountTreeView(account)); + } + } + + private static async accountTreeView(session: M365AuthenticationSession | undefined) { const accountCommands: ActionTreeItem[] = []; if (session) { commands.executeCommand('setContext', ContextKeys.isLoggedIn, true); - accountCommands.push(new ActionTreeItem(session.account.label, '', { name: 'M365', custom: true }, undefined, undefined, undefined, 'm365Account', [ - new ActionTreeItem('Sign out', '', { name: 'sign-out', custom: false }, undefined, Commands.logout) - ])); - CommandPanel.environmentTreeView(); + accountCommands.push(new ActionTreeItem(session.account.label, '', { name: 'spo-m365', custom: true }, undefined, undefined, undefined, 'm365Account', [])); + + const appCatalogUrls = await CliActions.appCatalogUrlsGet(); + if (appCatalogUrls?.some) { + const url = new URL(appCatalogUrls[0]); + const originUrl = url.origin; + const adminOriginUrl = originUrl.replace('.sharepoint.com', '-admin.sharepoint.com'); + const webApiPermissionManagementUrl = `${adminOriginUrl}/_layouts/15/online/AdminHome.aspx#/webApiPermissionManagement`; + DebuggerCheck.validateUrl(originUrl); + + accountCommands[0].children.push(new ActionTreeItem('SharePoint', '', { name: 'spo-logo', custom: true }, undefined, undefined, undefined, undefined, [ + new ActionTreeItem(originUrl.replace('https://',''), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(originUrl), 'sp-url'), + new ActionTreeItem(adminOriginUrl.replace('https://',''), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(adminOriginUrl), 'sp-admin-url'), + new ActionTreeItem(webApiPermissionManagementUrl.replace(`${adminOriginUrl}/_layouts/15/online/AdminHome.aspx#/`, '...'), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(webApiPermissionManagementUrl), 'sp-admin-api-url') + ])); + + const healthInfoList = await CliActions.getTenantHealthInfo(); + if (healthInfoList) + { + const healthInfoItems: ActionTreeItem[] = []; + for (let i = 0; i < healthInfoList.length; i++) { + healthInfoItems.push(new ActionTreeItem(healthInfoList[i].Title, '', { name: 'm365-warning', custom: true } , undefined, 'vscode.open', Uri.parse(healthInfoList[i].Url), 'm365-health-service-url')); + } + if (healthInfoItems.length > 0) { + accountCommands[0].children.push(new ActionTreeItem('Service health incidents', '', { name: 'm365-health', custom: true }, undefined, undefined, undefined, undefined, healthInfoItems)); + } + } + } + + accountCommands[0].children.push(new ActionTreeItem('Sign out', '', { name: 'sign-out', custom: false }, undefined, Commands.logout)); + CommandPanel.environmentTreeView(appCatalogUrls); } else { EnvironmentInformation.reset(); commands.executeCommand('setContext', ContextKeys.isLoggedIn, false); @@ -99,35 +134,36 @@ export class CommandPanel { accountCommands.push(new ActionTreeItem('Sign in to M365', '', { name: 'M365', custom: true }, undefined, Commands.login)); } - window.registerTreeDataProvider('pnp-view-account', new ActionTreeviewProvider(accountCommands)); + window.createTreeView('pnp-view-account', { treeDataProvider: new ActionTreeDataProvider(accountCommands), showCollapseAll: true }); } - /** - * Provide the actions for the environment treeview - */ - private static async environmentTreeView() { + private static async refreshEnvironmentTreeView(){ const appCatalogUrls = await CliActions.appCatalogUrlsGet(); + CommandPanel.environmentTreeView(appCatalogUrls); + } + private static async environmentTreeView(appCatalogUrls: string[] | undefined) { const environmentCommands: ActionTreeItem[] = []; if (!appCatalogUrls) { environmentCommands.push(new ActionTreeItem('No app catalog found', '')); } else { - const tenantAppCatalogUrl = appCatalogUrls[0]!; - const url = new URL(tenantAppCatalogUrl); + const tenantAppCatalogUrl = appCatalogUrls[0]; + const origin = new URL(tenantAppCatalogUrl).origin; commands.executeCommand('setContext', ContextKeys.hasAppCatalog, true); - const origin = url.origin; - DebuggerCheck.validateUrl(origin); - - AdaptiveCardCheck.validateACEComponent(); + const tenantWideExtensions = await CliActions.getTenantWideExtensions(tenantAppCatalogUrl); + const tenantWideExtensionsList: ActionTreeItem[] = []; + if (tenantWideExtensions && tenantWideExtensions?.length > 0) { + tenantWideExtensions.forEach((extension) => { + tenantWideExtensionsList.push(new ActionTreeItem(extension.Title, '', { name: 'spo-app', custom: true }, undefined, 'vscode.open', Uri.parse(extension.Url), 'sp-app-catalog-tenant-wide-extensions-url')); + }); + } environmentCommands.push( - new ActionTreeItem('SharePoint', '', { name: 'sharepoint', custom: true }, undefined, undefined, undefined, undefined, [ - new ActionTreeItem(origin, '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(origin), 'sp-url') - ]), - new ActionTreeItem('SharePoint Tenant App Catalog', '', { name: 'sharepoint', custom: true }, undefined, undefined, undefined, undefined, [ - new ActionTreeItem(tenantAppCatalogUrl.replace(origin, '...'), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(tenantAppCatalogUrl), 'sp-app-catalog-url') + new ActionTreeItem('Tenant App Catalog', '', { name: 'spo-logo', custom: true }, undefined, undefined, undefined, undefined, [ + new ActionTreeItem(tenantAppCatalogUrl.replace(origin, '...'), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(tenantAppCatalogUrl), 'sp-app-catalog-url'), + new ActionTreeItem('Tenant-wide Extensions', '', { name: 'spo-app-list', custom: true }, undefined, undefined, undefined, 'sp-app-catalog-tenant-wide-extensions', tenantWideExtensionsList) ]), ); @@ -136,16 +172,13 @@ export class CommandPanel { siteAppCatalogActionItems.push(new ActionTreeItem(appCatalogUrls[i].replace(origin, '...'), '', { name: 'globe', custom: false }, undefined, 'vscode.open', Uri.parse(appCatalogUrls[i]), 'sp-app-catalog-url')); } if (siteAppCatalogActionItems.length > 0) { - environmentCommands.push(new ActionTreeItem('SharePoint Site App Catalogs', '', { name: 'sharepoint', custom: true }, undefined, undefined, undefined, undefined, siteAppCatalogActionItems)); + environmentCommands.push(new ActionTreeItem('Site App Catalogs', '', { name: 'spo-logo', custom: true }, undefined, undefined, undefined, undefined, siteAppCatalogActionItems)); } } - window.createTreeView('pnp-view-environment', { treeDataProvider: new ActionTreeviewProvider(environmentCommands), showCollapseAll: true }); + window.createTreeView('pnp-view-environment', { treeDataProvider: new ActionTreeDataProvider(environmentCommands), showCollapseAll: true }); } - /** - * Provide the actions for the task treeview - */ private static taskTreeView() { const taskCommands: ActionTreeItem[] = [ new ActionTreeItem('Clean project', '', { name: 'debug-start', custom: false }, undefined, Commands.executeTerminalCommand, 'gulp clean'), @@ -158,12 +191,9 @@ export class CommandPanel { new ActionTreeItem('Serve from configuration', '', { name: 'debug-start', custom: false }, undefined, Commands.serveProject), ]; - window.registerTreeDataProvider('pnp-view-tasks', new ActionTreeviewProvider(taskCommands)); + window.registerTreeDataProvider('pnp-view-tasks', new ActionTreeDataProvider(taskCommands)); } - /** - * Provide the actions for the actions treeview - */ private static async actionsTreeView() { const actionCommands: ActionTreeItem[] = [ new ActionTreeItem('Upgrade project', '', { name: 'arrow-up', custom: false }, undefined, Commands.upgradeProject), @@ -176,12 +206,9 @@ export class CommandPanel { new ActionTreeItem('View samples', '', { name: 'library', custom: false }, undefined, Commands.samplesGallery), ]; - window.registerTreeDataProvider('pnp-view-actions', new ActionTreeviewProvider(actionCommands)); + window.registerTreeDataProvider('pnp-view-actions', new ActionTreeDataProvider(actionCommands)); } - /** - * Provide the actions for the help treeview - */ private static helpTreeView() { const helpCommands: ActionTreeItem[] = [ new ActionTreeItem('Docs & Learning', '', undefined, undefined, undefined, undefined, undefined, [ @@ -211,12 +238,9 @@ export class CommandPanel { ]) ]; - window.createTreeView('pnp-view-help', { treeDataProvider: new ActionTreeviewProvider(helpCommands), showCollapseAll: true }); + window.createTreeView('pnp-view-help', { treeDataProvider: new ActionTreeDataProvider(helpCommands), showCollapseAll: true }); } - /** - * Set the welcome view its context - */ private static showWelcome() { commands.executeCommand('setContext', ContextKeys.showWelcome, true); } diff --git a/src/providers/ActionTreeviewProvider.ts b/src/providers/ActionTreeDataProvider.ts similarity index 64% rename from src/providers/ActionTreeviewProvider.ts rename to src/providers/ActionTreeDataProvider.ts index 2332079..30326a5 100644 --- a/src/providers/ActionTreeviewProvider.ts +++ b/src/providers/ActionTreeDataProvider.ts @@ -1,9 +1,7 @@ -import { join } from 'path'; import { Event, ProviderResult, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Extension } from '../services/Extension'; -export class ActionTreeviewProvider implements TreeDataProvider { +export class ActionTreeDataProvider implements TreeDataProvider { onDidChangeTreeData?: Event | undefined; actions: ActionTreeItem[]; @@ -23,21 +21,15 @@ export class ActionTreeviewProvider implements TreeDataProvider { export class ActionTreeItem extends TreeItem { - constructor(label: string, description?: string, image?: { name: string; custom: boolean }, collapsibleState?: TreeItemCollapsibleState, command?: any, args?: any, contextValue?: string, private children?: ActionTreeItem[]) { - super(label, children ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); + children: ActionTreeItem[] = []; - const ext = Extension.getInstance(); - const extPath = ext.extensionPath; + constructor(label: string, description?: string, image?: { name: string; custom: boolean }, collapsibleState?: TreeItemCollapsibleState, command?: any, args?: any, contextValue?: string, children?: ActionTreeItem[]) { + super(label, children ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); this.label = label; this.description = description; - this.iconPath = image ? - !image.custom ? new ThemeIcon(image.name) : { - light: join(extPath, 'assets', 'icons', 'light', `${image.name}.svg`), - dark: join(extPath, 'assets', 'icons', 'dark', `${image.name}.svg`), - } - : undefined; + this.iconPath = image ? new ThemeIcon(image.name) : undefined; this.command = command ? { command: command, @@ -47,6 +39,8 @@ export class ActionTreeItem extends TreeItem { this.contextValue = contextValue; - this.children = children; + if (children) { + this.children = children; + } } } \ No newline at end of file diff --git a/src/services/CliActions.ts b/src/services/CliActions.ts index 902d60a..8b7608a 100644 --- a/src/services/CliActions.ts +++ b/src/services/CliActions.ts @@ -64,6 +64,45 @@ export class CliActions { return EnvironmentInformation.appCatalogUrls; } + public static async getTenantWideExtensions(tenantAppCatalogUrl: string): Promise<{Url: string, Title: string}[] | undefined> { + const origin = new URL(tenantAppCatalogUrl).origin; + const commandOptions: any = { + listUrl: `${tenantAppCatalogUrl.replace(origin, '')}/Lists/TenantWideExtensions`, + webUrl: tenantAppCatalogUrl + }; + const tenantWideExtensions = (await CliExecuter.execute('spo listitem list', 'json', commandOptions)).stdout || undefined; + + if (!tenantWideExtensions) { + return undefined; + } + + const tenantWideExtensionsJson: any[] = JSON.parse(tenantWideExtensions); + const tenantWideExtensionList = tenantWideExtensionsJson.map((extension) => { + return { + Url: `${tenantAppCatalogUrl}/Lists/TenantWideExtensions/DispForm.aspx?ID=${extension.Id}`, + Title: extension.Title + }; + }); + return tenantWideExtensionList; + } + + public static async getTenantHealthInfo(): Promise<{Title: string, Url: string}[] | undefined> { + const healthInfo = (await CliExecuter.execute('tenant serviceannouncement health list', 'json')).stdout || undefined; + + if (!healthInfo) { + return undefined; + } + + const healthInfoJson: any[] = JSON.parse(healthInfo); + const healthInfoList = healthInfoJson.filter(service => service.status !== 'serviceOperational').map((service) => { + return { + Url: `https://admin.microsoft.com/#/servicehealth/:/currentIssues/${encodeURIComponent(service.service)}/`, + Title: service.service + }; + }); + return healthInfoList; + } + public static async generateWorkflowForm(input: GenerateWorkflowCommandInput) { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -85,7 +124,7 @@ export class CliActions { if (!pfxBase64) { Notifications.error('Error generating certificate.'); - PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, {success: false}); + PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, { success: false }); return; } @@ -123,7 +162,7 @@ export class CliActions { } catch (e: any) { const message = e?.error?.message; Notifications.error(message); - PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, {success: false}); + PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, { success: false }); } }); } @@ -180,7 +219,7 @@ export class CliActions { } catch (e: any) { const message = e?.error?.message; Notifications.error(message); - PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, {success: false}); + PnPWebview.postMessage(WebviewCommand.toWebview.WorkflowCreated, { success: false }); } }); }