diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67bec7f7..7e2b791b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,14 +19,14 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: npm run build --if-present - run: npm run lint + - run: npm run build --if-present - run: npm test env: CI: true diff --git a/.gitignore b/.gitignore index 8267c195..2db7e1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules lib *.log +/lib spec/.tmp /certs /testGround \ No newline at end of file diff --git a/.snyk b/.snyk deleted file mode 100644 index 1e7a8954..00000000 --- a/.snyk +++ /dev/null @@ -1,4 +0,0 @@ -# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. -version: v1.14.1 -ignore: {} -patch: {} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index d892d55d..9da55205 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "request": "launch", "name": "Launch Program", "cwd": "${workspaceRoot}", - "program": "${workspaceFolder}\\bin\\pbiviz-new.js", + "program": "${workspaceFolder\bin\\pbiviz-new.js", "args": [ "-t", "circlecard", diff --git a/Changelog.md b/Changelog.md index 7c19c25d..e137315d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,9 @@ This page contains information about changes to the PowerBI Visual Tools (pbiviz). +## 5.2.0 +* Integrated PAC validation + ## 5.1.0 * New flag `--skip-api` to skip verifying api version. It might produce different errors in visual, so use it only in some specific cases (ex. installing something during the build process brakes packages managed by monorepo managers). * New flag `--all-locales` to disable optimization using localization loader. It's recommended not to use this flag because all locales take a huge amount of package size. If you need just a few of them follow [this guide](https://learn.microsoft.com/en-us/power-bi/developer/visuals/localization?tabs=English#step-5---add-a-resources-file-for-each-language). In this case, only declared in stringResources locales will be added to your visual package. diff --git a/bin/pbiviz.js b/bin/pbiviz.js index c271f69d..f6241d85 100755 --- a/bin/pbiviz.js +++ b/bin/pbiviz.js @@ -97,4 +97,4 @@ pbiviz CommandManager.package(options, rootPath); }); -program.parse(process.argv); \ No newline at end of file +program.parse(process.argv); diff --git a/package-lock.json b/package-lock.json index 75546945..209b4e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "powerbi-visuals-tools", - "version": "5.1.0", + "version": "5.2.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "powerbi-visuals-tools", - "version": "5.1.0", + "version": "5.2.0-beta.1", "license": "MIT", "dependencies": { "@typescript-eslint/parser": "^5.62.0", @@ -34,6 +34,7 @@ "lodash.clonedeep": "4.5.0", "lodash.defaults": "4.2.0", "lodash.isequal": "4.5.0", + "lodash.ismatch": "^4.4.0", "mini-css-extract-plugin": "^2.7.6", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", @@ -3721,6 +3722,11 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index b51f1257..ed838af2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "powerbi-visuals-tools", - "version": "5.1.0", + "version": "5.2.0-beta.1", "description": "Command line tool for creating and publishing visuals for Power BI", "main": "./bin/pbiviz.js", "type": "module", @@ -54,6 +54,7 @@ "lodash.clonedeep": "4.5.0", "lodash.defaults": "4.2.0", "lodash.isequal": "4.5.0", + "lodash.ismatch": "^4.4.0", "mini-css-extract-plugin": "^2.7.6", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", diff --git a/spec/unit/FeatureManagerSpec.js b/spec/unit/FeatureManagerSpec.js new file mode 100644 index 00000000..03d08c9e --- /dev/null +++ b/spec/unit/FeatureManagerSpec.js @@ -0,0 +1,115 @@ +/* + * Power BI Visual CLI + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the ""Software""), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +"use strict"; + +import * as features from '../../lib/features/index.js'; +import Package from '../../lib/Package.js'; +import { Stage, VisualFeatureType } from '../../lib/features/FeatureTypes.js'; +import { readJsonFromRoot } from '../../lib/utils.js'; + +const config = readJsonFromRoot('config.json'); + +describe("Features", () => { + describe("Visual", () => { + const { APIVersion, ESLint, VisualVersion } = features; + it("Should support API Version", () => { + const Visual = { + doesAPIVersionMatch: (minVersion) => { + expect(minVersion).toBe(config.constants.minAPIversion); + return true; + } + } + expect(APIVersion.isSupported(Visual)).toBeTrue; + }); + + it("Should support ESLint", () => { + const Visual = { + doesESLlintSupported: () => true + } + expect(ESLint.isSupported(Visual)).toBeTrue; + }); + + it("Should support Version", () => { + const Visual = { + isVisualVersionValid: (versionLength) => { + expect(versionLength).toBe(4); + return true; + } + } + expect(VisualVersion.isSupported(Visual)).toBeTrue; + }); + }); + + describe("Package", () => { + const featuresArray = Object.keys(features).filter(key => features[key].stage === Stage.PostBuild).map(key => features[key]); + + it("Should support features with correct sources", () => { + const sourceCode = `.allowInteractions, .applySelectionFromFilter or .registerOnSelectCallback, .colorPalette, + .createDataViewWildcardSelector, .showContextMenu, .downloadService and .exportVisualsContent, + getFormattingModel, .isHighContrast, .launchUrl, .createLocalizationManager, .storageService, .openModalDialog, + .eventService and .renderingStarted and .renderingFinished, tooltipService, .displayWarningIcon` + const capabilities = { + advancedEditMode: 1, + supportsHighlight: true, + supportsKeyboardFocus: true, + supportsLandingPage: true, + supportsMultiVisualSelection: true, + supportsSynchronizingFilterState: true, + subtotals: true, + tooltips: {}, + objects: { + objectCategory: 2 + }, + drilldown: { + roles: [] + }, + dataViewMappings: [ + { + table: { + rows: { + dataReductionAlgorithm: {} + } + } + } + ] + } + const correctPackage = new Package(sourceCode, capabilities, VisualFeatureType.All); + + featuresArray.forEach(feature => { + expect(feature.isSupported(correctPackage)).toBeTrue; + }) + }); + + it("Should not support features with empty sources", () => { + const emptyPackage = new Package('', {}, VisualFeatureType.All); + + featuresArray.forEach(feature => { + expect(feature.isSupported(emptyPackage)).toBeFalse; + }) + }); + }); +}); diff --git a/src/CommandManager.ts b/src/CommandManager.ts index 15550f1b..76fc1e6d 100644 --- a/src/CommandManager.ts +++ b/src/CommandManager.ts @@ -45,6 +45,7 @@ export default class CommandManager { const visualManager = new VisualManager(rootPath) await visualManager .prepareVisual() + .validateVisual() .initializeWebpack(webpackOptions) visualManager.startWebpackServer(options.drop) } @@ -68,6 +69,7 @@ export default class CommandManager { } new VisualManager(rootPath) .prepareVisual() + .validateVisual() .initializeWebpack(webpackOptions) .then(visualManager => visualManager.generatePackage()) } diff --git a/src/FeatureManager.ts b/src/FeatureManager.ts new file mode 100644 index 00000000..8b5b78bb --- /dev/null +++ b/src/FeatureManager.ts @@ -0,0 +1,58 @@ +import { Severity, Stage } from "./features/FeatureTypes.js"; +import * as features from "./features/index.js"; +import { Visual } from "./Visual.js"; +import Package from "./Package.js"; + +export enum Status { + Success, + Error +} +export interface ValidationStats { + status: Status, + logs: Logs +} + +export interface Logs { + errors: string[], + warnings: string[], + info: string[], + deprecation: string[] +} + +export class FeatureManager { + public features = Object.keys(features).map(key => features[key]); + + public validate(stage: Stage, sourceInstance: Visual | Package): ValidationStats { + const result: ValidationStats = { + status: Status.Success, + logs: { + errors: [], + warnings: [], + info: [], + deprecation: [] + } + } + this.features + .filter(feature => feature.stage == stage) + .filter(feature => feature.visualFeatureType & sourceInstance.visualFeatureType) + .filter(feature => !feature.isSupported(sourceInstance)) + .forEach(({ errorMessage, severity }) => { + switch(severity) { + case Severity.Error: + result.status = Status.Error; + result.logs.errors.push(errorMessage); + break; + case Severity.Warning: + result.logs.warnings.push(errorMessage); + break; + case Severity.Info: + result.logs.info.push(errorMessage); + break; + case Severity.Deprecation: + result.logs.deprecation.push(errorMessage); + break; + } + }); + return result + } +} \ No newline at end of file diff --git a/src/Package.ts b/src/Package.ts new file mode 100644 index 00000000..3617a6b8 --- /dev/null +++ b/src/Package.ts @@ -0,0 +1,53 @@ +/* + * Power BI Visual CLI + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the ""Software""), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +"use strict"; + +import { VisualFeatureType } from "./features/FeatureTypes.js"; +import isMatch from "lodash.ismatch"; + +/** + * Represents an instance of a visual package based on file path + */ +export default class Package { + private sourceCode: string; + private capabilities: object; + public visualFeatureType: VisualFeatureType; + + constructor(sourceCode: string, capabilities: object, visualFeatureType: VisualFeatureType) { + this.sourceCode = sourceCode; + this.capabilities = capabilities; + this.visualFeatureType = visualFeatureType; + } + + public contain(keyword: string) { + return this.sourceCode.includes(keyword); + } + + public isCapabilityEnabled(expectedObject: object) { + return isMatch(this.capabilities, expectedObject); + } +} diff --git a/src/Visual.ts b/src/Visual.ts new file mode 100644 index 00000000..abe1dc61 --- /dev/null +++ b/src/Visual.ts @@ -0,0 +1,40 @@ +import { compareVersions } from "compare-versions"; +import { VisualFeatureType } from "./features/FeatureTypes.js"; + +export class Visual { + public visualFeatureType: VisualFeatureType; + private capabilities; + private config; + private packageJSON; + private visualVersion: string; + + constructor(capabilities, config, packageJson) { + this.capabilities = capabilities; + this.config = config; + this.visualFeatureType = this.getVisualFeatureType(); + this.packageJSON = packageJson; + this.visualVersion = config.visual.version; + } + + public doesAPIVersionMatch(minAPIversion: string) { + return compareVersions(this.config.apiVersion ?? minAPIversion, minAPIversion) !== -1 + } + + public doesESLlintSupported() { + return Object.entries(this.packageJSON.scripts).some(([, value]) => (value).includes("eslint")) + } + + public isVisualVersionValid(length: number) { + return this.visualVersion.split(".").length === length + } + + private getVisualFeatureType() { + const isMatrixSupported = this.capabilities?.dataViewMappings?.some(dataView => dataView.matrix) + const isSlicer = Boolean(this.capabilities?.objects?.general?.properties?.filter?.type?.filter) + let type = isSlicer ? VisualFeatureType.Slicer : VisualFeatureType.NonSlicer; + if (isMatrixSupported) { + type = type | VisualFeatureType.Matrix; + } + return type; + } +} \ No newline at end of file diff --git a/src/VisualManager.ts b/src/VisualManager.ts index 29a373cd..f223e3b1 100644 --- a/src/VisualManager.ts +++ b/src/VisualManager.ts @@ -36,6 +36,10 @@ import ConsoleWriter from './ConsoleWriter.js'; import VisualGenerator from './VisualGenerator.js'; import { readJsonFromRoot, readJsonFromVisual } from './utils.js'; import WebpackWrap, { WebpackOptions } from './WebPackWrap.js'; +import Package from './Package.js'; +import { Visual } from "./Visual.js"; +import { FeatureManager, Logs, Status } from "./FeatureManager.js"; +import { Severity, Stage } from "./features/FeatureTypes.js"; import TemplateFetcher from "./TemplateFetcher.js"; interface GenerateOptions { @@ -43,6 +47,7 @@ interface GenerateOptions { template: string; } +const globalConfig = readJsonFromRoot('config.json'); const PBIVIZ_FILE = 'pbiviz.json'; /** @@ -51,7 +56,11 @@ const PBIVIZ_FILE = 'pbiviz.json'; export default class VisualManager { public basePath: string; public pbivizConfig; + private capabilities; private webpackConfig; + private visual: Visual; + private package: Package; + private featureManager: FeatureManager; private compiler: Compiler; private webpackDevServer: WebpackDevServer; @@ -62,6 +71,7 @@ export default class VisualManager { public prepareVisual() { if (this.doesPBIVIZExists()) { this.pbivizConfig = readJsonFromVisual(PBIVIZ_FILE, this.basePath); + this.createVisualInstance(); } else { ConsoleWriter.error(PBIVIZ_FILE + ' not found. You must be in the root of a visual project to run this command.') process.exit(1); @@ -69,6 +79,12 @@ export default class VisualManager { return this; } + public createVisualInstance() { + this.capabilities = readJsonFromVisual("capabilities.json", this.basePath); + const packageJSON = readJsonFromVisual("package.json", this.basePath); + this.visual = new Visual(this.capabilities, this.pbivizConfig, packageJSON); + } + public async initializeWebpack(webpackOptions: WebpackOptions) { const webpackWrap = new WebpackWrap(); this.webpackConfig = await webpackWrap.generateWebpackConfig(this, webpackOptions) @@ -80,6 +96,9 @@ export default class VisualManager { public generatePackage() { const callback = (err: Error, stats: Stats) => { + this.createPackageInstance(); + const logs = this.validatePackage(); + this.outputResults(logs); this.parseCompilationResults(err, stats) } this.compiler.run(callback); @@ -115,6 +134,37 @@ export default class VisualManager { } } + public validateVisual() { + this.featureManager = new FeatureManager() + const { status, logs } = this.featureManager.validate(Stage.PreBuild, this.visual); + this.outputResults(logs); + if(status === Status.Error){ + process.exit(1); + } + + return this; + } + + public validatePackage() { + const featureManager = new FeatureManager(); + const { logs } = featureManager.validate(Stage.PostBuild, this.package); + + return logs; + } + + public outputResults({ errors, deprecation, warnings, info }: Logs) { + const featuresTotalLog = { + errors: `Visual doesn't support some features required for all custom visuals:`, + deprecation: `Some features are going to be required soon, please update the visual:`, + warn: `Visual doesn't support some features recommended for all custom visuals:`, + info: `Visual can be improved by adding some features:` + }; + this.outputLogsWithHeadMessage(featuresTotalLog.errors, errors, Severity.Error); + this.outputLogsWithHeadMessage(featuresTotalLog.deprecation, deprecation, Severity.Deprecation); + this.outputLogsWithHeadMessage(featuresTotalLog.warn, warnings, Severity.Warning); + this.outputLogsWithHeadMessage(featuresTotalLog.info, info, Severity.Info); + } + public displayInfo() { if (this.pbivizConfig) { ConsoleWriter.infoTable(this.pbivizConfig); @@ -166,7 +216,7 @@ export default class VisualManager { }); }); } - + private doesPBIVIZExists() { return fs.existsSync(PBIVIZ_FILE); } @@ -205,6 +255,12 @@ export default class VisualManager { } } + private createPackageInstance() { + const pathToJSContent = path.join((this.pbivizConfig.build ?? globalConfig.build).dropFolder, "visual.js"); + const sourceCode = fs.readFileSync(pathToJSContent, "utf8"); + this.package = new Package(sourceCode, this.capabilities, this.visual.visualFeatureType); + } + private parseCompilationResults(err: Error, stats: Stats) { ConsoleWriter.blank(); if (err) { @@ -218,4 +274,30 @@ export default class VisualManager { ConsoleWriter.done('Build completed successfully'); } } + + private outputLogsWithHeadMessage(headMessage: string, logs: string[], severity: Severity) { + if(!logs.length) { + return; + } + let outputLog; + switch(severity) { + case Severity.Error || Severity.Deprecation: + outputLog = ConsoleWriter.error; + break; + case Severity.Warning: + outputLog = ConsoleWriter.warning; + break; + default: + outputLog = ConsoleWriter.info; + break; + } + + if(headMessage) { + outputLog(headMessage); + ConsoleWriter.blank(); + } + + logs.forEach(error => outputLog(error)); + ConsoleWriter.blank(); + } } diff --git a/src/features/APIVersion.ts b/src/features/APIVersion.ts new file mode 100644 index 00000000..05a57619 --- /dev/null +++ b/src/features/APIVersion.ts @@ -0,0 +1,20 @@ +import { Visual } from "../Visual.js"; +import { readJsonFromRoot } from "../utils.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class APIVersion implements BaseFeature { + public static featureName = "Api" + public static severity = Severity.Error + public static stage = Stage.PreBuild + public static visualFeatureType = VisualFeatureType.NonSlicer + public static minAPIversion: string; + public static errorMessage: string; + + static isSupported(visual: Visual) { + const globalConfig = readJsonFromRoot('config.json'); + this.minAPIversion = globalConfig.constants.minAPIversion; + this.errorMessage = `API version must be at least ${this.minAPIversion}.` + return visual.doesAPIVersionMatch(this.minAPIversion) + } +} \ No newline at end of file diff --git a/src/features/AdvancedEditMode.ts b/src/features/AdvancedEditMode.ts new file mode 100644 index 00000000..9fb5c081 --- /dev/null +++ b/src/features/AdvancedEditMode.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class AdvancedEditMode implements BaseFeature { + public static featureName = "Advanced Edit Mode" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/advanced-edit-mode" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return !packageInstance.isCapabilityEnabled({ advancedEditMode: 0 }) // 0 - Advanced edit mode is disabled + } +} \ No newline at end of file diff --git a/src/features/AllowInteractions.ts b/src/features/AllowInteractions.ts new file mode 100644 index 00000000..95ad0b7d --- /dev/null +++ b/src/features/AllowInteractions.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class AllowInteractions implements BaseFeature { + public static featureName = "Allow Interactions" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/visuals-interactions" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain('.allowInteractions') + } +} \ No newline at end of file diff --git a/src/features/AnalyticsPane.ts b/src/features/AnalyticsPane.ts new file mode 100644 index 00000000..fb418075 --- /dev/null +++ b/src/features/AnalyticsPane.ts @@ -0,0 +1,22 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class AnalyticsPane implements BaseFeature { + public static featureName = "Analytics Pane" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/analytics-pane" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return ( + packageInstance.isCapabilityEnabled({ + objects: { + objectCategory: 2 + } + }) || + packageInstance.contain("analyticsPane=true") + ) + } +} \ No newline at end of file diff --git a/src/features/BaseFeature.ts b/src/features/BaseFeature.ts new file mode 100644 index 00000000..c8f24337 --- /dev/null +++ b/src/features/BaseFeature.ts @@ -0,0 +1,14 @@ +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default abstract class BaseFeature { + public static severity: Severity + public static stage: Stage + public static visualFeatureType: VisualFeatureType + public static featureName: string + public static documentationLink: string + public static get errorMessage() { + return `${this.featureName} - ${this.documentationLink}` + } + + protected static isSupported() {} +} diff --git a/src/features/Bookmarks.ts b/src/features/Bookmarks.ts new file mode 100644 index 00000000..3d1757d2 --- /dev/null +++ b/src/features/Bookmarks.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class Bookmarks implements BaseFeature { + public static featureName = "Bookmarks" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/bookmarks-support" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain("applySelectionFromFilter") || packageInstance.contain("registerOnSelectCallback") + } +} \ No newline at end of file diff --git a/src/features/ColorPalette.ts b/src/features/ColorPalette.ts new file mode 100644 index 00000000..8c2fa076 --- /dev/null +++ b/src/features/ColorPalette.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class ColorPalette implements BaseFeature { + public static featureName = "Color Palette" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/add-colors-power-bi-visual" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".colorPalette") + } +} \ No newline at end of file diff --git a/src/features/ConditionalFormatting.ts b/src/features/ConditionalFormatting.ts new file mode 100644 index 00000000..3a18fa77 --- /dev/null +++ b/src/features/ConditionalFormatting.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class ConditionalFormatting implements BaseFeature { + public static featureName = "Conditional Formatting" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/conditional-format" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".createDataViewWildcardSelector") + } +} \ No newline at end of file diff --git a/src/features/ContextMenu.ts b/src/features/ContextMenu.ts new file mode 100644 index 00000000..30782de1 --- /dev/null +++ b/src/features/ContextMenu.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class ContextMenu implements BaseFeature { + public static featureName = "Context Menu" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/context-menu" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".showContextMenu") + } +} \ No newline at end of file diff --git a/src/features/DrillDown.ts b/src/features/DrillDown.ts new file mode 100644 index 00000000..70cbc875 --- /dev/null +++ b/src/features/DrillDown.ts @@ -0,0 +1,19 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class DrillDown implements BaseFeature { + public static featureName = "Drill Down" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/drill-down-support" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ + drilldown: { + roles: [] + } + }) + } +} \ No newline at end of file diff --git a/src/features/ESLint.ts b/src/features/ESLint.ts new file mode 100644 index 00000000..d454f718 --- /dev/null +++ b/src/features/ESLint.ts @@ -0,0 +1,15 @@ +import { Visual } from "../Visual.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class ESLint implements BaseFeature { + public static featureName = "ESLint" + public static documentationLink = "https://github.com/microsoft/eslint-plugin-powerbi-visuals"; + public static severity = Severity.Error + public static stage = Stage.PreBuild + public static visualFeatureType = VisualFeatureType.All + + static isSupported(visual: Visual) { + return visual.doesESLlintSupported() + } +} \ No newline at end of file diff --git a/src/features/FeatureTypes.ts b/src/features/FeatureTypes.ts new file mode 100644 index 00000000..1c25db2d --- /dev/null +++ b/src/features/FeatureTypes.ts @@ -0,0 +1,24 @@ +export enum Severity { + Error = "error", + Deprecation = "deprecation", + Warning = "warning", + Info = "info", +} + +export enum Stage { + PreBuild = "pre-build", + PostBuild = "post-build", +} + +export enum VisualFeatureType { + NonSlicer = 1 << 1, + Slicer = 1 << 2, + Matrix = 1 << 3, + All = NonSlicer | Slicer | Matrix +} + +// Interaction types: Selection or filter (slicer) +// Slicer type: Basic, Advanced, Tuple filter, Identity filter + +// Visual Type: TS/JS or R-Visual or RHTML +// Dataview Type: Single or Matrix or Table or Category or All \ No newline at end of file diff --git a/src/features/FetchMoreData.ts b/src/features/FetchMoreData.ts new file mode 100644 index 00000000..9e29c1cb --- /dev/null +++ b/src/features/FetchMoreData.ts @@ -0,0 +1,25 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class FetchMoreData implements BaseFeature { + public static featureName = "Fetch More Data" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/fetch-more-data" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ + dataViewMappings: [ + { + table: { + rows: { + dataReductionAlgorithm: {} + } + } + } + ] + }) + } +} \ No newline at end of file diff --git a/src/features/FileDownload.ts b/src/features/FileDownload.ts new file mode 100644 index 00000000..b696afc1 --- /dev/null +++ b/src/features/FileDownload.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class FileDownload implements BaseFeature { + public static featureName = "File Download" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/file-download-api" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".downloadService") && packageInstance.contain(".exportVisualsContent") + } +} \ No newline at end of file diff --git a/src/features/FormatPane.ts b/src/features/FormatPane.ts new file mode 100644 index 00000000..e1748417 --- /dev/null +++ b/src/features/FormatPane.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class FormatPane implements BaseFeature { + public static featureName = "Format Pane" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/format-pane" + public static severity = Severity.Deprecation + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.All + + static isSupported(packageInstance: Package) { + return packageInstance.contain("getFormattingModel") + } +} \ No newline at end of file diff --git a/src/features/HighContrast.ts b/src/features/HighContrast.ts new file mode 100644 index 00000000..91a18793 --- /dev/null +++ b/src/features/HighContrast.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class HighContrast implements BaseFeature { + public static featureName = "High Contrast" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/high-contrast-support" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".isHighContrast") + } +} \ No newline at end of file diff --git a/src/features/HighlightData.ts b/src/features/HighlightData.ts new file mode 100644 index 00000000..6f84b99e --- /dev/null +++ b/src/features/HighlightData.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class HighlightData implements BaseFeature { + public static featureName = "Highlight Data" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/highlight" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ supportsHighlight: true }) + } +} \ No newline at end of file diff --git a/src/features/KeyboardNavigation.ts b/src/features/KeyboardNavigation.ts new file mode 100644 index 00000000..5e11a013 --- /dev/null +++ b/src/features/KeyboardNavigation.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class KeyboardNavigation implements BaseFeature { + public static featureName = "Keyboard Navigation" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/supportskeyboardfocus-feature" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ supportsKeyboardFocus: true }) + } +} \ No newline at end of file diff --git a/src/features/LandingPage.ts b/src/features/LandingPage.ts new file mode 100644 index 00000000..d23c0fac --- /dev/null +++ b/src/features/LandingPage.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class LandingPage implements BaseFeature { + public static featureName = "Landing Page" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/landing-page" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ supportsLandingPage: true }) + } +} \ No newline at end of file diff --git a/src/features/LaunchURL.ts b/src/features/LaunchURL.ts new file mode 100644 index 00000000..72c2d22a --- /dev/null +++ b/src/features/LaunchURL.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class LaunchURL implements BaseFeature { + public static featureName = "Launch URL" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/launch-url" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".launchUrl") + } +} \ No newline at end of file diff --git a/src/features/LocalStorage.ts b/src/features/LocalStorage.ts new file mode 100644 index 00000000..17b59950 --- /dev/null +++ b/src/features/LocalStorage.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class LocalStorage implements BaseFeature { + public static featureName = "Local Storage" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/local-storage" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".storageService") + } +} \ No newline at end of file diff --git a/src/features/Localizations.ts b/src/features/Localizations.ts new file mode 100644 index 00000000..446f0fb1 --- /dev/null +++ b/src/features/Localizations.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class Localizations implements BaseFeature { + public static featureName = "Localizations" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/localization" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".createLocalizationManager") + } +} \ No newline at end of file diff --git a/src/features/ModalDialog.ts b/src/features/ModalDialog.ts new file mode 100644 index 00000000..9a027cee --- /dev/null +++ b/src/features/ModalDialog.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class ModalDialog implements BaseFeature { + public static featureName = "Modal Dialog" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/create-display-dialog-box" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".openModalDialog") + } +} \ No newline at end of file diff --git a/src/features/RenderingEvents.ts b/src/features/RenderingEvents.ts new file mode 100644 index 00000000..09f511ea --- /dev/null +++ b/src/features/RenderingEvents.ts @@ -0,0 +1,16 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class RenderingEvents implements BaseFeature { + public static featureName = "Rendering Events" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/event-service" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.All + + static isSupported(packageInstance: Package) { + const keywords = [".eventService", ".renderingStarted", ".renderingFinished"] + return !keywords.some(keyword => !packageInstance.contain(keyword)) + } +} \ No newline at end of file diff --git a/src/features/SelectionAcrossVisuals.ts b/src/features/SelectionAcrossVisuals.ts new file mode 100644 index 00000000..264c8df1 --- /dev/null +++ b/src/features/SelectionAcrossVisuals.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class SelectionAcrossVisuals implements BaseFeature { + public static featureName = "Selection Across Visuals" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/supportsmultivisualselection-feature" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ supportsMultiVisualSelection: true }) + } +} \ No newline at end of file diff --git a/src/features/SyncSlicer.ts b/src/features/SyncSlicer.ts new file mode 100644 index 00000000..5d0ae321 --- /dev/null +++ b/src/features/SyncSlicer.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class SyncSlicer implements BaseFeature { + public static featureName = "Sync Slicer" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/enable-sync-slicers" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ supportsSynchronizingFilterState: true }) + } +} \ No newline at end of file diff --git a/src/features/Tooltips.ts b/src/features/Tooltips.ts new file mode 100644 index 00000000..3166b996 --- /dev/null +++ b/src/features/Tooltips.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class Tooltips implements BaseFeature { + public static featureName = "Tooltips" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/add-tooltips" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain("tooltipService") && packageInstance.isCapabilityEnabled({tooltips: {}}) + } +} \ No newline at end of file diff --git a/src/features/TotalSubTotal.ts b/src/features/TotalSubTotal.ts new file mode 100644 index 00000000..d05f6dc7 --- /dev/null +++ b/src/features/TotalSubTotal.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class TotalSubTotal implements BaseFeature { + public static featureName = "Total SubTotal" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/total-subtotal-api" + public static severity = Severity.Warning + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.Matrix | VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.isCapabilityEnabled({ subtotals: true }) + } +} \ No newline at end of file diff --git a/src/features/VisualVersion.ts b/src/features/VisualVersion.ts new file mode 100644 index 00000000..88386420 --- /dev/null +++ b/src/features/VisualVersion.ts @@ -0,0 +1,16 @@ +import { Visual } from "../Visual.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class VisualVersion implements BaseFeature { + public static featureName = "Visual version" + public static expectedVersionLength = 4; + public static errorMessage = `${this.featureName} should consist of ${this.expectedVersionLength} parts. Update the pbiviz.json file`; + public static severity = Severity.Error + public static stage = Stage.PreBuild + public static visualFeatureType = VisualFeatureType.All + + static isSupported(visual: Visual) { + return visual.isVisualVersionValid(this.expectedVersionLength) + } +} \ No newline at end of file diff --git a/src/features/WarningIcon.ts b/src/features/WarningIcon.ts new file mode 100644 index 00000000..f019d204 --- /dev/null +++ b/src/features/WarningIcon.ts @@ -0,0 +1,15 @@ +import Package from "../Package.js"; +import BaseFeature from "./BaseFeature.js"; +import { Severity, Stage, VisualFeatureType } from "./FeatureTypes.js"; + +export default class WarningIcon implements BaseFeature { + public static featureName = "Warning Icon" + public static documentationLink = "https://learn.microsoft.com/en-us/power-bi/developer/visuals/visual-display-warning-icon" + public static severity = Severity.Info + public static stage = Stage.PostBuild + public static visualFeatureType = VisualFeatureType.NonSlicer | VisualFeatureType.Slicer + + static isSupported(packageInstance: Package) { + return packageInstance.contain(".displayWarningIcon") + } +} \ No newline at end of file diff --git a/src/features/index.ts b/src/features/index.ts new file mode 100644 index 00000000..e4a28b23 --- /dev/null +++ b/src/features/index.ts @@ -0,0 +1,38 @@ +import AdvancedEditMode from './AdvancedEditMode.js' +import AllowInteractions from './AllowInteractions.js' +import AnalyticsPane from './AnalyticsPane.js' +import Bookmarks from './Bookmarks.js' +import ColorPalette from './ColorPalette.js' +import ConditionalFormatting from './ConditionalFormatting.js' +import ContextMenu from './ContextMenu.js' +import DrillDown from './DrillDown.js' +import FetchMoreData from './FetchMoreData.js' +import FileDownload from './FileDownload.js' +import FormatPane from './FormatPane.js' +import HighContrast from './HighContrast.js' +import HighlightData from './HighlightData.js' +import KeyboardNavigation from './KeyboardNavigation.js' +import LandingPage from './LandingPage.js' +import LaunchURL from './LaunchURL.js' +import Localizations from './Localizations.js' +import LocalStorage from './LocalStorage.js' +import ModalDialog from './ModalDialog.js' +import RenderingEvents from './RenderingEvents.js' +import SelectionAcrossVisuals from './SelectionAcrossVisuals.js' +import SyncSlicer from './SyncSlicer.js' +import Tooltips from './Tooltips.js' +import TotalSubTotal from './TotalSubTotal.js' +import WarningIcon from './WarningIcon.js' +import APIVersion from './APIVersion.js' +import ESLint from './ESLint.js' +import VisualVersion from './VisualVersion.js' + +export { + AdvancedEditMode, AllowInteractions, AnalyticsPane, Bookmarks, + ColorPalette, ConditionalFormatting, ContextMenu, DrillDown, + FetchMoreData, FileDownload, FormatPane, HighContrast, + HighlightData, KeyboardNavigation, LandingPage, LaunchURL, + Localizations, LocalStorage, ModalDialog, RenderingEvents, + SelectionAcrossVisuals, SyncSlicer, Tooltips, TotalSubTotal, + WarningIcon, APIVersion, ESLint, VisualVersion +} \ No newline at end of file