diff --git a/docusaurus/.gitignore b/docusaurus/.gitignore index b2d6de30624..4ca5fe40ff2 100644 --- a/docusaurus/.gitignore +++ b/docusaurus/.gitignore @@ -8,6 +8,9 @@ .docusaurus .cache-loader +# Typedoc integration +docs/extensions/shell-api + # Misc .DS_Store .env.local diff --git a/docusaurus/docs/extensions/screenshots/intellisense.png b/docusaurus/docs/extensions/screenshots/intellisense.png new file mode 100644 index 00000000000..3333217aab4 Binary files /dev/null and b/docusaurus/docs/extensions/screenshots/intellisense.png differ diff --git a/docusaurus/docs/extensions/shell-api.md b/docusaurus/docs/extensions/shell-api.md new file mode 100644 index 00000000000..43f2eecca76 --- /dev/null +++ b/docusaurus/docs/extensions/shell-api.md @@ -0,0 +1,53 @@ +--- +id: shell-api +--- + +# Shell API + +> Available from Rancher `2.13` and onwards + +## What is the Shell API? + +The Shell API is a functional API that helps extension developers to interact with UI elements that are included in Rancher UI that are important for extension development. We have paid special attention on the implementation side of the architecture behind this API so that developers can use features such as Intellisense and auto-completion in your favourite IDE. + +![Intellisense](./screenshots/intellisense.png) + +## How to use the Shell API + +### Using Options API in Vue + +To use the Shell API in the context of the **Options API** of a Vue component, we globally expose the `$shell` property, which is available under the `this` object, such as: + +```ts +import { NotificationLevel } from '@shell/types/notifications'; + +this.$shell.notification.send(NotificationLevel.Success, 'Some notification title', 'Hello world! Success!', {}) +``` + +### Using Composition API in Vue + +To use the Shell API in the context of the **Composition API** of a Vue component, we'll need to import the correct method to make the API available in the component: + +```ts +import { useShell } from '@shell/apis'; +``` + +then just assign to a constant in the context for your component and use it, such as: + +```ts +import { NotificationLevel } from '@shell/types/notifications'; + +const shellApi = useShell(); + +// Example method to display a Growl message +const sendNotification = () => shellApi.notification.send(NotificationLevel.Success, 'Some notification title', 'Hello world! Success!', {}) +``` + +## Available API's + +| API | Description | Example | +| :--- | :--- | :--- | +| [Slide-In API](./shell-api/interfaces/SlideInApi) | Responsible for interacting with slide-in panels | slidein example | +| [Modal API](./shell-api/interfaces/ModalApi) | Responsible for interacting with modals | modal example | +| [Notification API](./shell-api/interfaces/NotificationApi) | Responsible for interacting with the Rancher UI Notification Center | notification example | +| [System API](./shell-api/interfaces/SystemApi) | API for system related information | system example | diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index eca69d2f825..595a5b9175d 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -43,6 +43,53 @@ const config = { ], plugins: [ + [ + 'docusaurus-plugin-typedoc', + { + // Set to true to hide the source path information entirely. + disableSources: true, + + // This removes the prefix from the title of a generated page + // Ex: "Interface: ModalApi" vs "ModalApi" + textContentMappings: { 'title.memberPage': '{name}' }, + + // show params as code blocks + useCodeBlocks: true, + + // The entry point for TypeDoc to start scanning for files. + // The path is relative to the `docusaurus` directory. + // entryPoints: ['../shell/apis/shell/*'], + entryPoints: ['../shell/apis/intf/shell.ts'], + + // The tsconfig file for TypeDoc to use. + // This is CRITICAL to point to our special, isolated config + // to avoid the thousands of compilation errors from the main project. + tsconfig: 'tsconfig-typedoc-integration.json', + + // The output directory for the generated markdown files. + // We are targeting the specific versioned folder for the "next" version + // and placing it directly into the "advanced" subfolder. + out: 'docs/extensions/shell-api', + + // disables REAME as default entry point + readme: 'none', + + // prevents deletion of files that are in "out" directory + cleanOutputDir: false, + + // allows auto-generation of the sidebar for the auto-generated items + sidebar: { autoConfiguration: true }, + + // since we can't prevent the generation of a "bad" index file, we rename it to something harmless + entryFileName: '_api-index.md', + + // A unique ID for this plugin instance. Needed so that it generates all the files successfully (including sidebar entries) + id: 'api-docs', + + // exclude all code comments marked as @internal + excludeInternal: true + }, + ], [require.resolve('docusaurus-lunr-search'), { excludeRoutes: ['internal/*', 'internal/**/*', '/internal/*', '/internal/**/*', 'blog/*', 'blog/**/*', '/blog/*', '/blog/**/*'] } ], [ diff --git a/docusaurus/extensionSidebar.js b/docusaurus/extensionSidebar.js index 2504dde3768..80f45c7c08a 100644 --- a/docusaurus/extensionSidebar.js +++ b/docusaurus/extensionSidebar.js @@ -1,3 +1,67 @@ +const fs = require('fs'); +const path = require('path'); + +// Define the absolute path to the sidebar file that `docusaurus-plugin-typedoc` generates. +const typedocSidebarPath = path.resolve(__dirname, './docs/extensions/shell-api/typedoc-sidebar.cjs'); + +/** + * Recursively processes an array of Docusaurus sidebar items. + * It finds any 'doc' items and removes the incorrect 'extensions/' prefix + * from their ID, which is a known issue with the plugin in multi-instance setups. + * + * @param {Array} items The array of sidebar items from the generated file. + * @returns {Array} The corrected array of sidebar items. + */ +function fixTypedocIds(items) { + return items.map((item) => { + // 1. Start with a shallow copy of the item. + const newItem = { ...item }; + + // Remove 'extensions/' prefix from 'doc' IDs + if (newItem.type === 'doc' && newItem.id && newItem.id.startsWith('extensions/')) { + newItem.id = newItem.id.replace('extensions/', ''); + } + + // RECURSION: Process sub-items if it's a 'category' + if (newItem.type === 'category' && newItem.items) { + newItem.items = fixTypedocIds(newItem.items); + } + + // Remove 'link' property if it points to an '_api-index' document (auto-generated by the plugin... we discard them) + const link = newItem.link; + + if ( + link && + typeof link === 'object' && link !== null && + link.id && + typeof link.id === 'string' && + link.id.endsWith('_api-index') + ) { + // Use object destructuring to create a new object without the 'link' property + // and return it immediately. + const { link: removedLink, ...rest } = newItem; + + return rest; + } + + // Return the (potentially) modified item. + return newItem; + }); +} + +// Initialize an empty array for the TypeDoc sidebar items. +let typedocSidebarItems = []; + +// Safely check if the generated `typedoc-sidebar.cjs` file exists. +// This prevents build errors if the file hasn't been generated yet. +if (fs.existsSync(typedocSidebarPath)) { + // If the file exists, import its contents. + const originalTypedocSidebar = require(typedocSidebarPath); + + // Run the imported items through our correction function. + typedocSidebarItems = fixTypedocIds(originalTypedocSidebar); +} + /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { extensionsSidebar: [ @@ -51,6 +115,15 @@ const sidebars = { ], }], }, + { + type: 'category', + label: 'Shell API', + link: { + type: 'doc', + id: 'shell-api', + }, + items: typedocSidebarItems + }, { type: 'category', label: 'Extensions API', @@ -123,7 +196,7 @@ const sidebars = { 'advanced/stores', 'advanced/version-compatibility', 'advanced/safe-mode', - 'advanced/yarn-link', + 'advanced/yarn-link' ] }, 'publishing', diff --git a/docusaurus/package.json b/docusaurus/package.json index 676fce8f1da..496e2ec1139 100644 --- a/docusaurus/package.json +++ b/docusaurus/package.json @@ -24,11 +24,17 @@ "prism-react-renderer": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "yaml": "2.5.1" + "yaml": "2.5.1", + "docusaurus-plugin-typedoc": "^1.4.2", + "typedoc": "^0.28.13", + "typedoc-plugin-markdown": "^4.9.0", + "typescript": "^5.9.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.6.3", - "@docusaurus/types": "3.0.0" + "@docusaurus/types": "3.0.0", + "vue": "3.5.18", + "vue-router": "4.5.1" }, "browserslist": { "production": [ diff --git a/docusaurus/src/css/custom.css b/docusaurus/src/css/custom.css index c6c9c1a8b89..c5134a5108e 100644 --- a/docusaurus/src/css/custom.css +++ b/docusaurus/src/css/custom.css @@ -176,6 +176,19 @@ a.gettingStartedLink .resourcesSvg svg { hr { margin: 2rem 0; } + +/* Fixes for CSS issues with auto generated docs */ +/* Fixes anchor styling methods subtitle */ +h4.anchor { + font-size: 26px; +} + +/* Fixes anchor styling for method params */ +h5.anchor { + font-size: 20px; + text-decoration: underline; +} + @media screen and (max-width: 996px) { .homepage-banner { padding: 30px 30px; diff --git a/docusaurus/static/img/growl.png b/docusaurus/static/img/growl.png new file mode 100644 index 00000000000..3c382f46f42 Binary files /dev/null and b/docusaurus/static/img/growl.png differ diff --git a/docusaurus/static/img/modal.png b/docusaurus/static/img/modal.png new file mode 100644 index 00000000000..3b3e0723a63 Binary files /dev/null and b/docusaurus/static/img/modal.png differ diff --git a/docusaurus/static/img/notification.png b/docusaurus/static/img/notification.png new file mode 100644 index 00000000000..1d24b9ec1f1 Binary files /dev/null and b/docusaurus/static/img/notification.png differ diff --git a/docusaurus/static/img/notifications/error.png b/docusaurus/static/img/notifications/error.png new file mode 100644 index 00000000000..e332a962cab Binary files /dev/null and b/docusaurus/static/img/notifications/error.png differ diff --git a/docusaurus/static/img/notifications/success.png b/docusaurus/static/img/notifications/success.png new file mode 100644 index 00000000000..5cb429b4551 Binary files /dev/null and b/docusaurus/static/img/notifications/success.png differ diff --git a/docusaurus/static/img/notifications/warning.png b/docusaurus/static/img/notifications/warning.png new file mode 100644 index 00000000000..574e25c7dfa Binary files /dev/null and b/docusaurus/static/img/notifications/warning.png differ diff --git a/docusaurus/static/img/slidein.png b/docusaurus/static/img/slidein.png new file mode 100644 index 00000000000..06320835bb5 Binary files /dev/null and b/docusaurus/static/img/slidein.png differ diff --git a/docusaurus/static/img/system.png b/docusaurus/static/img/system.png new file mode 100644 index 00000000000..103753ae618 Binary files /dev/null and b/docusaurus/static/img/system.png differ diff --git a/docusaurus/tsconfig-typedoc-integration.json b/docusaurus/tsconfig-typedoc-integration.json new file mode 100644 index 00000000000..d3dc128ff19 --- /dev/null +++ b/docusaurus/tsconfig-typedoc-integration.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.paths.json", + "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "skipLibCheck": true, + }, + "include": [ + "../shell/apis/intf/shell.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/docusaurus/tsconfig.paths.json b/docusaurus/tsconfig.paths.json new file mode 100644 index 00000000000..aafc3936257 --- /dev/null +++ b/docusaurus/tsconfig.paths.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "paths": { + // we need to explicitly tell docusaurus where to look for vue types... + "vue": [ + "node_modules/vue" + ], + "vue-router": [ + "node_modules/vue-router" + ], + "~/*": [ + "../*" + ], + "@/*": [ + "../*" + ], + "@shell/*": [ + "../shell/*" + ], + "@pkg/*": [ + "../shell/pkg/*" + ], + "@components/*": [ + "../pkg/rancher-components/src/components/*" + ] + } + } +} \ No newline at end of file diff --git a/pkg/rancher-prime/tsconfig.json b/pkg/rancher-prime/tsconfig.json index 8bcc2a1e780..6cf1b31b937 100644 --- a/pkg/rancher-prime/tsconfig.json +++ b/pkg/rancher-prime/tsconfig.json @@ -46,7 +46,8 @@ "**/*.ts", "**/*.d.ts", "**/*.tsx", - "**/*.vue" + "**/*.vue", + "../../shell/types/vue-shim.d.ts" ], "exclude": [ "../../node_modules" diff --git a/shell/apis/impl/apis.ts b/shell/apis/impl/apis.ts new file mode 100644 index 00000000000..78249d2f61e --- /dev/null +++ b/shell/apis/impl/apis.ts @@ -0,0 +1,61 @@ +import { throttle } from 'lodash'; +import { createExtensionManager } from '@shell/core/extension-manager-impl'; +import { ShellApiImpl } from '@shell/apis/shell'; + +/** + * Initialise the APIs that are available in the shell + * + * This is loaded during app startiup in `initialize/index.js` + */ +export function initUiApis(context: any, inject: any, vueApp: any) { + // ====================================================================================================================== + // Extension Manager + // ====================================================================================================================== + const extensionManager = createExtensionManager(context); + const deprecationMessage = '[DEPRECATED] `this.$plugin` is deprecated and will be removed in a future version. Use `this.$extension` instead.'; + + registerApi('plugin', deprecationProxy(extensionManager, deprecationMessage), inject, vueApp); + registerApi('extension', extensionManager, inject, vueApp); + + // ====================================================================================================================== + // Shell API + // ====================================================================================================================== + registerApi('shell', new ShellApiImpl(context.store), inject, vueApp); +} + +// ====================================================================================================================== +// Helpers +// ====================================================================================================================== + +function registerApi(name: string, api: any, inject: any, vueApp: any) { + inject(name, api); + vueApp.provide(`$${ name }`, api); +} + +/** + * Proxy to log a deprecation warning when target is accessed. Only prints + * deprecation warnings in dev builds. + * @param {*} target the object to proxy + * @param {*} message the deprecation warning to print to the console + * @returns The proxied target that prints a deprecation warning when target is + * accessed + */ +const deprecationProxy = (target: any, message: string) => { + const logWarning = throttle(() => { + // eslint-disable-next-line no-console + console.warn(message); + }, 150); + + const deprecationHandler = { + get(target: any, prop: any) { + logWarning(); + + return Reflect.get(target, prop); + } + }; + + // an empty handler allows the proxy to behave just like the original target + const proxyHandler = !!process.env.dev ? deprecationHandler : {}; + + return new Proxy(target, proxyHandler); +}; diff --git a/shell/apis/index.ts b/shell/apis/index.ts new file mode 100644 index 00000000000..878f2bcf89b --- /dev/null +++ b/shell/apis/index.ts @@ -0,0 +1,40 @@ +// Main export for APIs, particularly for the composition API + +import { inject } from 'vue'; +import { ExtensionManager } from '@shell/types/extension-manager'; +import { ShellApi as ShellApiImport } from '@shell/apis/intf/shell'; + +// Re-export the types for the APIs, so they appear in this module +export type ShellApi = ShellApiImport; +export type ExtensionManagerApi = ExtensionManager; + +/** + * Provides access to the registered extension manager instance. + * + * @returns The extension manager API + */ +export const useExtensionManager = (): ExtensionManagerApi => { + return getApi('$extension', 'useExtensionManager'); +}; + +/** + * Returns the Shell API + * + * @returns The shell API + */ +export const useShell = (): ShellApi => { + return getApi('$shell', 'useShell'); +}; + +// ================================================================================================================= +// Internal helper to get any API by key with error handling +// ================================================================================================================= +function getApi(key: string, name: string): T { + const api = inject(key); + + if (!api) { + throw new Error(`${ name } must only be called after ${ key } has been initialized`); + } + + return api as T; +} diff --git a/shell/apis/intf/shell.ts b/shell/apis/intf/shell.ts new file mode 100644 index 00000000000..2ce560d7bc2 --- /dev/null +++ b/shell/apis/intf/shell.ts @@ -0,0 +1,396 @@ +import { Component } from 'vue'; +import { RouteLocationRaw } from 'vue-router'; +import { NotificationLevel } from '@shell/types/notifications'; + +/** + * Configuration object for opening a modal. + */ +export interface ModalApiConfig { + /** + * Props to pass directly to the component rendered inside the modal. + * + * Example: + * ```ts + * componentProps: { title: 'Hello Modal', isVisible: true } + * ``` + */ + componentProps?: Record; + + /** + * Array of resources that the modal component might need. + * These are passed directly into the modal's `resources` prop. + * + * Example: + * ```ts + * resources: [myResource, anotherResource] + * ``` + */ + resources?: any[]; + + /** + * Custom width for the modal. Defaults to `600px`. + * The width can be specified as a string with a valid unit (`px`, `%`, `rem`, etc.). + * + * Examples: + * ```ts + * modalWidth: '800px' // Width in pixels + * modalWidth: '75%' // Width as a percentage + * ``` + */ + modalWidth?: string; + + /** + * Determines if clicking outside the modal will close it. Defaults to `true`. + * Set this to `false` to prevent closing via outside clicks. + * + * Example: + * ```ts + * closeOnClickOutside: false + * ``` + */ + closeOnClickOutside?: boolean; + + /** + * If true, the modal is considered "sticky" and may not close automatically + * on certain user interactions. Defaults to `false`. + * + * Example: + * ```ts + * modalSticky: true + * ``` + */ + // modalSticky?: boolean; // Not implemented yet +} + +/** + * API for displaying modals in Rancher UI. Here's what a Modal looks like in Rancher UI: + * * ![modal Example](/img/modal.png) + */ +export interface ModalApi { + /** + * Opens a modal dialog in Rancher UI + * + * Example: + * ```ts + * import MyCustomModal from '@/components/MyCustomModal.vue'; + * + * this.$shell.modal.show(MyCustomModal, { + * componentProps: { title: 'Hello Modal' } + * }); + * ``` + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param component + * The Vue component to be displayed inside the modal. + * This can be any SFC (Single-File Component) imported and passed in as a `Component`. + * + * + * @param config Modal configuration object + * + */ + open(component: Component, config?: ModalApiConfig): void; +} + +/** + * + * Configuration object for opening a Slide-In panel. Here's what a Slide-In looks like in Rancher UI: + * + */ +export interface SlideInApiConfig { + /** + * + * Width of the Slide In panel in percentage, related to the window width. Defaults to `33%` + * + */ + width?: string; + /** + * + * Height of the Slide In panel. Can be percentage or vh. Defaults to (window - header) height. + * Can be set as `33%` or `80vh` + * + */ + height?: string; + /** + * + * CSS Top position for the Slide In panel, string using px, as `0px` or `20px`. Default is right below header height + * + */ + top?: string; + /** + * + * title for the Slide In panel + * + */ + title?: string; + /** + * + * Wether Slide In header is displayed or not + * + */ + showHeader?: boolean; + /** + * + * Props to pass directly to the component rendered inside the slide in panel in an object format + * + */ + componentProps?: Record; +} + +/** + * API for displaying Slide In panels in Rancher UI + * * ![slidein Example](/img/slidein.png) + */ +export interface SlideInApi { + /** + * Opens a slide in panel in Rancher UI + * + * Example: + * ```ts + * import MyCustomSlideIn from '@/components/MyCustomSlideIn.vue'; + * + * this.$shell.slideIn.open(MyCustomSlideIn, { + * title: 'Hello from SlideIn panel!' + * }); + * ``` + * + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param component + * The Vue component to be displayed inside the slide in panel. + * This can be any SFC (Single-File Component) imported and passed in as a `Component`. + * + * @param config Slide-In configuration object + * + */ + open(component: Component, config?: SlideInApiConfig): void; +} + +/** + * Notification Action definition + */ +export interface NotificationApiAction { + /** + * Button label for the action + */ + label: string; + /** + * Href target when the button is clicked + */ + target?: string; + /** + * Vue Route to navigate to when the button is clicked + */ + route?: RouteLocationRaw; +} + +/** + * Notification Preference definition + */ +export interface NotificationApiPreference { + /** + * User preference key to use when setting the preference when the notification is marked as read + */ + key: string; + /** + * User preference value to use when setting the preference when the notification is marked as read + */ + value: string; + /** + * User preference value to use when setting the preference when the notification is marked as unread - defaults to empty string + */ + unsetValue?: string; +} + +/** + * Configuration object for the Notification Center + * + */ +export interface NotificationApiConfig { + /** + * - **{@link NotificationApiAction}** + * + * Primary action to be shown in the notification + */ + primaryAction?: NotificationApiAction; + /** + * - **{@link NotificationApiAction}** + * + * Secondary to be shown in the notification + */ + secondaryAction?: NotificationApiAction; + /** + * Unique ID for the notification + */ + id?: string; + /** + * Progress (0-100) for notifications of type `Task` + */ + progress?: number; + /** + * - **{@link NotificationApiPreference}** + * + * User Preference tied to the notification (the preference will be updated when the notification is marked read) + */ + preference?: NotificationApiPreference; +} + +/** + * Notification Level for a notification in the Notification Center + */ +export enum NotificationApiLevel { + /** + * An announcement. To be used when we want to inform on high-interest topics - news, updates, changes, scheduled maintenance, etc. E.g. “New version available!” + */ + Announcement = 0, // eslint-disable-line no-unused-vars + /** + * A task that is underway. To be used when we want to inform on a process taking place - on-going actions that might take a while. E.g. “Cluster provisioning in progress”. The progress bar will also be shown if the `progress` field is set + */ + Task, // eslint-disable-line no-unused-vars + /** + * Information notification. To be used when we want to inform on low-interest topics. E.g. “Welcome to Rancher v2.8" + */ + Info, // eslint-disable-line no-unused-vars + /** + * Notification that something has completed successfully. To be used when we want to confirm a successful action was completed. E.g. “Cluster provisioning completed” + * + * A notification of type `Success` will also show a growl notication on Rancher UI + * * ![success Example](/img/notifications/success.png) + */ + Success, // eslint-disable-line no-unused-vars + /** + * Notification of a warning. To be used when we want to warn about a potential risk. E.g. “Nodes limitation warning” + * + * A notification of type `Warning` will also show a growl notication on Rancher UI + * * ![warning Example](/img/notifications/warning.png) + */ + Warning, // eslint-disable-line no-unused-vars + /** + * Notification of an error. To be used when we want to alert on a confirmed risk. E.g. “Extension failed to load” + * + * A notification of type `Error` will also show a growl notication on Rancher UI + * * ![error Example](/img/notifications/error.png) + */ + Error, // eslint-disable-line no-unused-vars +} + +/** + * API for notifications in the Rancher UI Notification Center + * * ![notification Example](/img/notification.png) + */ +export interface NotificationApi { + /** + * Sends a notification to the Rancher UI Notification Center + * + * Example: + * ```ts + * import { NotificationLevel } from '@shell/types/notifications'; + * + * this.$shell.notification.send(NotificationLevel.Success, 'Some notification title', 'Hello world! Success!', {}) + * ``` + * + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param level The `level` specifies the importance of the notification and determines the icon that is shown in the notification + * @param title The notification title + * @param message The notification message to be displayed + * @param config Notifications configuration object + * + * @returns notification ID + * + */ + send(level: NotificationApiLevel | NotificationLevel, title: string, message?:string, config?: NotificationApiConfig): Promise; + + /** + * Update notification progress (Only valid for notifications of type `Task`) + * + * Example: + * ```ts + * this.$shell.notification.updateProgress('some-notification-id', 80) + * ``` + * + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param notificationId Unique ID for the notification + * @param progress Progress (0-100) for notifications of type `Task` + * + */ + + updateProgress(notificationId: string, progress: number): void; +} + +/** + * API for system related information + * * ![system Example](/img/system.png) + */ +export interface SystemApi { + /** + * Rancher version + */ + rancherVersion: string; + /** + * Rancher UI version + */ + uiVersion: string; + /** + * Rancher CLI version + */ + cliVersion: string; + /** + * Rancher Helm version + */ + helmVersion: string; + /** + * Rancher Docker Machine version + */ + machineVersion: string; + /** + * If Rancher system running is Prime + */ + isRancherPrime: boolean; + /** + * Git Commit for Rancher system running + */ + gitCommit: string; + /** + * Rancher Kubernetes version + */ + kubernetesVersion: string; + /** + * Rancher build platform + */ + buildPlatform: string; + /** + * If Rancher system is a Dev build + */ + isDevBuild: boolean; + /** + * If Rancher system is a Pre-Release build/version + */ + isPrereleaseVersion: boolean; +} + +/** + * @internal + * Available "API's" inside Shell API + */ +export interface ShellApi { + /** + * Provides access to the Modal API + */ + get modal(): ModalApi; + + /** + * Provides access to the Slide-In API + */ + get slideIn(): SlideInApi; + + /** + * Provides access to the Notification Center API + */ + get notification(): NotificationApi; + + /** + * Provides access to the System API + */ + get system(): SystemApi; +} diff --git a/shell/apis/shell/index.ts b/shell/apis/shell/index.ts new file mode 100644 index 00000000000..f220ad47942 --- /dev/null +++ b/shell/apis/shell/index.ts @@ -0,0 +1,38 @@ +import { Store } from 'vuex'; +import { + ModalApi, ShellApi, SlideInApi, NotificationApi, SystemApi +} from '@shell/apis/intf/shell'; +import { ModalApiImpl } from './modal'; +import { SlideInApiImpl } from './slide-in'; +import { NotificationApiImpl } from './notifications'; +import { SystemApiImpl } from './system'; + +export class ShellApiImpl implements ShellApi { + private modalApi: ModalApi; + private slideInApi: SlideInApi; + private notificationApi: NotificationApi; + private systemApi: SystemApi; + + constructor(store: Store) { + this.modalApi = new ModalApiImpl(store); + this.slideInApi = new SlideInApiImpl(store); + this.notificationApi = new NotificationApiImpl(store); + this.systemApi = new SystemApiImpl(store); + } + + get modal(): ModalApi { + return this.modalApi; + } + + get slideIn(): SlideInApi { + return this.slideInApi; + } + + get notification(): NotificationApi { + return this.notificationApi; + } + + get system(): SystemApi { + return this.systemApi; + } +} diff --git a/shell/apis/shell/modal.ts b/shell/apis/shell/modal.ts new file mode 100644 index 00000000000..dac265e1171 --- /dev/null +++ b/shell/apis/shell/modal.ts @@ -0,0 +1,41 @@ +import { Component } from 'vue'; +import { ModalApi, ModalApiConfig } from '@shell/apis/intf/shell'; +import { Store } from 'vuex'; + +export class ModalApiImpl implements ModalApi { + private store: Store; + + constructor(store: Store) { + this.store = store; + } + + /** + * Opens a modal by committing to the Vuex store. + * + * Example: + * ```ts + * import MyCustomModal from '@/components/MyCustomModal.vue'; + * + * this.$shell.modal.show(MyCustomModal, { + * componentProps: { title: 'Hello Modal' } + * }); + * ``` + * + * @param component + * The Vue component to be displayed inside the modal. + * This can be any SFC (Single-File Component) imported and passed in as a `Component`. + * + * @param config Configuration object for opening a modal. + * + */ + public open(component: Component, config?: ModalApiConfig): void { + this.store.commit('modal/openModal', { + component, + componentProps: config?.componentProps || {}, + resources: config?.resources || [], + modalWidth: config?.modalWidth || '600px', + closeOnClickOutside: config?.closeOnClickOutside ?? true, + // modalSticky: config.modalSticky ?? false // Not implemented yet + }); + } +} diff --git a/shell/apis/shell/notifications.ts b/shell/apis/shell/notifications.ts new file mode 100644 index 00000000000..10ad5d1f50b --- /dev/null +++ b/shell/apis/shell/notifications.ts @@ -0,0 +1,66 @@ +import { Store } from 'vuex'; +import { NotificationApiLevel, NotificationApi, NotificationApiConfig } from '@shell/apis/intf/shell'; +import { NotificationLevel } from '@shell/types/notifications'; + +export class NotificationApiImpl implements NotificationApi { + private store: Store; + + constructor(store: Store) { + this.store = store; + } + + /** + * Sends a notification to the Rancher UI Notification Center + * + * Example: + * ```ts + * import { NotificationLevel } from '@shell/types/notifications'; + * + * this.$shell.notification.send(NotificationLevel.Success, 'Some notification title', 'Hello world! Success!', {}) + * ``` + * + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param level The `level` specifies the importance of the notification and determines the icon that is shown in the notification + * @param title The notification title + * @param message The notification message to be displayed + * @param config Notifications configuration object + * + * * @returns notification ID + * + */ + public async send(level: NotificationApiLevel | NotificationLevel, title: string, message?:string, config?: NotificationApiConfig): Promise { + const notification = { + level, + title, + message, + ...config + }; + + return await this.store.dispatch('notifications/add', notification, { root: true }); + } + + /** + * Update notification progress (Only valid for notifications of type `Task`) + * + * Example: + * ```ts + * this.$shell.notification.updateProgress('some-notification-id', 80) + * ``` + * + * For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue). + * + * @param notificationId Unique ID for the notification + * @param progress Progress (0-100) for notifications of type `Task` + * + */ + + public updateProgress(notificationId: string, progress: number): void { + const notification = { + id: notificationId, + progress + }; + + this.store.dispatch('notifications/update', notification, { root: true }); + } +} diff --git a/shell/apis/shell/slide-in.ts b/shell/apis/shell/slide-in.ts new file mode 100644 index 00000000000..8d0157a62dc --- /dev/null +++ b/shell/apis/shell/slide-in.ts @@ -0,0 +1,33 @@ +import { Component } from 'vue'; +import { SlideInApi, SlideInApiConfig } from '@shell/apis/intf/shell'; +import { Store } from 'vuex'; + +export class SlideInApiImpl implements SlideInApi { + private store: Store; + + constructor(store: Store) { + this.store = store; + } + + /** + * Opens a slide in panel in Rancher UI + * + * Example: + * ```ts + * import MyCustomSlideIn from '@/components/MyCustomSlideIn.vue'; + * + * this.$shell.slideIn.open(MyCustomSlideIn, { + * title: 'Hello from SlideIn panel!' + * }); + * ``` + * + * @param component - A Vue component (imported SFC, functional component, etc.) to be rendered in the panel. + * @param config - Slide-In configuration object + */ + public open(component: Component, config?: SlideInApiConfig): void { + this.store.commit('slideInPanel/open', { + component, + slideInConfig: config || {} + }); + } +} diff --git a/shell/apis/shell/system.ts b/shell/apis/shell/system.ts new file mode 100644 index 00000000000..c5ee4f3712d --- /dev/null +++ b/shell/apis/shell/system.ts @@ -0,0 +1,97 @@ +import { SystemApi } from '@shell/apis/intf/shell'; +import { Store } from 'vuex'; +import { MANAGEMENT } from '@shell/config/types'; +import { SETTING } from '@shell/config/settings'; +import { isDevBuild, isPrerelease } from '@shell/utils/version'; +import { getVersionData, getKubeVersionData, isRancherPrime } from '@shell/config/version'; + +export class SystemApiImpl implements SystemApi { + private store: Store; + + constructor(store: Store) { + this.store = store; + } + + /** + * Rancher version + */ + get rancherVersion(): string { + return getVersionData().Version; + } + + /** + * Rancher UI version + */ + get uiVersion(): string { + const storeTyped = this.store as any; + + return storeTyped.$config.dashboardVersion; + } + + /** + * Rancher CLI version + */ + get cliVersion() { + return this.store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_CLI)?.value || ''; + } + + /** + * Rancher Helm version + */ + get helmVersion() { + return this.store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_HELM)?.value || ''; + } + + /** + * Rancher Docker Machine version + */ + get machineVersion() { + return this.store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_MACHINE)?.value || ''; + } + + /** + * If Rancher system running is Prime + */ + get isRancherPrime(): boolean { + return isRancherPrime(); + } + + /** + * Git Commit for Rancher system running + */ + get gitCommit(): string { + return getVersionData().GitCommit; + } + + /** + * Rancher Kubernetes version + */ + get kubernetesVersion(): string { + const kubeData = getKubeVersionData() as any || {}; + + return kubeData.gitVersion; + } + + /** + * Rancher build platform + */ + get buildPlatform(): string { + const kubeData = getKubeVersionData() as any || {}; + + return kubeData.platform; + } + + /** + * If Rancher system is a Dev build + */ + get isDevBuild(): boolean { + return isDevBuild(this.rancherVersion); + } + + /** + * If Rancher system is a Pre-Release build/version + */ + get isPrereleaseVersion(): boolean { + return isPrerelease(this.rancherVersion); + } +} diff --git a/shell/apis/vue-shim.d.ts b/shell/apis/vue-shim.d.ts new file mode 100644 index 00000000000..e0736ef46d7 --- /dev/null +++ b/shell/apis/vue-shim.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +import { ShellApi, ExtensionManagerApi } from '@shell/apis'; + +export {}; + +declare module 'vue' { + interface ComponentCustomProperties { + $shell: ShellApi, + $extension: ExtensionManagerApi, + } +} \ No newline at end of file diff --git a/shell/components/ModalManager.vue b/shell/components/ModalManager.vue index 241aab01b32..366aa6d90bd 100644 --- a/shell/components/ModalManager.vue +++ b/shell/components/ModalManager.vue @@ -5,6 +5,7 @@ import { useStore } from 'vuex'; import AppModal from '@shell/components/AppModal.vue'; const store = useStore(); +const componentRendered = ref(false); const isOpen = computed(() => store.getters['modal/isOpen']); const component = computed(() => store.getters['modal/component']); @@ -23,12 +24,20 @@ function close() { backgroundClosing.value(); } + componentRendered.value = false; store.commit('modal/closeModal'); } function registerBackgroundClosing(fn: Function) { backgroundClosing.value = fn; } + +function onSlotComponentMounted() { + // variable for the watcher based focus-trap + // so that we know when the component is rendered + // works in tandem with trigger-focus-trap="true" + componentRendered.value = true; +}