From f7254db753fd7987aee5e0eb5fe4064484878e10 Mon Sep 17 00:00:00 2001 From: Viktor Tsvetkov <142901247+vtsvetkov-splunk@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:25:11 +0100 Subject: [PATCH] chore: preparation to NPM publish (#1511) --- .github/workflows/build-ui.yml | 2 + ui/package.json | 27 ++++++-- .../components/BaseFormView/BaseFormView.tsx | 12 ++-- .../CheckBoxComponent/CheckBoxComponent.tsx | 2 +- .../stories/ControlWrapper.stories.ts | 2 +- .../test/ControlWrapper.test.tsx | 2 +- .../CustomControl/CustomControl.tsx | 52 ++++++---------- .../CustomControl/CustomControl.types.ts | 8 +++ .../CustomControl/CustomControlBase.ts | 43 +++++++++++++ ui/src/components/CustomTab/CustomTab.tsx | 16 ++--- .../components/CustomTab/CustomTab.types.ts | 4 ++ ui/src/components/CustomTab/CustomTabBase.ts | 21 +++++++ ui/src/components/DeleteModal/DeleteModal.tsx | 2 +- ui/src/components/EntityPage/EntityPage.tsx | 2 +- .../TextAreaComponent/TextAreaComponent.tsx | 2 +- .../TextComponent/TextComponent.tsx | 2 +- ui/src/publicApi.ts | 11 ++++ ui/src/types/components/BaseFormTypes.ts | 46 +------------- ui/src/types/components/CustomHookClass.ts | 61 +++++++++++++++++++ ui/tsconfig.lib.json | 11 ++++ 20 files changed, 221 insertions(+), 107 deletions(-) create mode 100644 ui/src/components/CustomControl/CustomControl.types.ts create mode 100644 ui/src/components/CustomControl/CustomControlBase.ts create mode 100644 ui/src/components/CustomTab/CustomTab.types.ts create mode 100644 ui/src/components/CustomTab/CustomTabBase.ts create mode 100644 ui/src/publicApi.ts create mode 100644 ui/src/types/components/CustomHookClass.ts create mode 100644 ui/tsconfig.lib.json diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml index 2e1ac0641..cce4f3de0 100644 --- a/.github/workflows/build-ui.yml +++ b/.github/workflows/build-ui.yml @@ -17,6 +17,8 @@ jobs: run: yarn run lint - name: Unit test run: yarn run test + - name: Build UCC library + run: yarn run build:lib - name: Build UCC UI run: yarn run build - name: List deps into dependencies.txt diff --git a/ui/package.json b/ui/package.json index c011772d4..568971bf6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,10 +1,18 @@ { - "name": "@splunk/ucc_ui_lib", - "version": "0.0.1", + "name": "@splunk/add-on-ucc-framework", + "description": "UCC framework is a build and code generation framework for building Splunk Add-ons (TAs)", + "repository": { + "type": "git", + "url": "git+https://github.com/splunk/addonfactory-ucc-generator.git", + "directory": "ui" + }, + "version": "5.56.0", "license": "Apache-2.0", - "private": true, + "author": "Splunk Inc.", + "homepage": "https://splunk.github.io/addonfactory-ucc-generator", "scripts": { "build": "cross-env NODE_ENV=production webpack --bail", + "build:lib": "cross-env NODE_ENV=production tsc --project tsconfig.lib.json", "build:watch": "webpack --watch", "format": "prettier \"src/**/*.(js|jsx|ts|tsx|css)\" --write", "format:verify": "prettier \"src/**/*.(js|jsx|ts|tsx|css)\" --list-different", @@ -18,7 +26,8 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test-storybook": "test-storybook", - "test-storybook:update-snapshots": "yarn run test-storybook -u" + "test-storybook:update-snapshots": "yarn run test-storybook -u", + "prepublishOnly": "yarn run build:lib" }, "dependencies": { "@splunk/dashboard-action-buttons": "^28.0.0", @@ -119,6 +128,10 @@ "webpack-dev-server": "^5.2.0", "webpack-merge": "^6.0.1" }, + "peerDependencies": { + "react": "^16.14.0", + "typescript": "^5.6.3" + }, "resolutions": { "@npmcli/git": "^2.1.0", "@types/react": "^16.14.62", @@ -139,5 +152,9 @@ "workerDirectory": [ "src/public" ] - } + }, + "types": "./dist/lib/publicApi.d.ts", + "files": [ + "dist/lib" + ] } diff --git a/ui/src/components/BaseFormView/BaseFormView.tsx b/ui/src/components/BaseFormView/BaseFormView.tsx index 37b3558b4..9adc26495 100644 --- a/ui/src/components/BaseFormView/BaseFormView.tsx +++ b/ui/src/components/BaseFormView/BaseFormView.tsx @@ -37,12 +37,10 @@ import { UtilControlWrapper, ServiceGroup, OauthConfiguration, - CustomHook, AnyEntity, OAuthEntity, BasicEntity, ChangeRecord, - CustomHookClass, EntitiesAllowingModifications, } from '../../types/components/BaseFormTypes'; import { @@ -51,6 +49,7 @@ import { } from '../FormModifications/FormModifications'; import { GlobalConfig } from '../../types/globalConfig/globalConfig'; import { shouldHideForPlatform } from '../../util/pageContext'; +import { CustomHookConstructor, CustomHookInstance } from '../../types/components/CustomHookClass'; function onCustomHookError(params: { methodName: string; error?: CustomHookError }) { // eslint-disable-next-line no-console @@ -107,7 +106,7 @@ class BaseFormView extends PureComponent { datadict: Record; - hook?: CustomHook; + hook?: CustomHookInstance; // eslint-disable-next-line camelcase state_enabled?: boolean; @@ -1072,7 +1071,7 @@ class BaseFormView extends PureComponent { if (type === 'external') { import(/* webpackIgnore: true */ `${getBuildDirPath()}/custom/${module}.js`).then( (external) => { - const Hook = external.default; + const Hook = external.default as CustomHookConstructor; this.hook = new Hook( globalConfig, this.props.serviceName, @@ -1088,13 +1087,14 @@ class BaseFormView extends PureComponent { // @ts-expect-error should be exported to other js module and imported here __non_webpack_require__( [`app/${this.appName}/js/build/custom/${module}`], - (Hook: CustomHookClass) => { + (Hook: CustomHookConstructor) => { this.hook = new Hook( globalConfig, this.props.serviceName, this.state, this.props.mode, - this.util + this.util, + this.props.groupName ); resolve(Hook); } diff --git a/ui/src/components/CheckBoxComponent/CheckBoxComponent.tsx b/ui/src/components/CheckBoxComponent/CheckBoxComponent.tsx index 514060e82..ca9a05732 100644 --- a/ui/src/components/CheckBoxComponent/CheckBoxComponent.tsx +++ b/ui/src/components/CheckBoxComponent/CheckBoxComponent.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Switch from '@splunk/react-ui/Switch'; import { isFalse } from '../../util/considerFalseAndTruthy'; -interface CheckBoxComponentProps { +export interface CheckBoxComponentProps { value: 0 | 1 | boolean; handleChange: (field: string, value: 0 | 1) => void; field: string; diff --git a/ui/src/components/ControlWrapper/stories/ControlWrapper.stories.ts b/ui/src/components/ControlWrapper/stories/ControlWrapper.stories.ts index dec319531..1e6e52a18 100644 --- a/ui/src/components/ControlWrapper/stories/ControlWrapper.stories.ts +++ b/ui/src/components/ControlWrapper/stories/ControlWrapper.stories.ts @@ -20,7 +20,7 @@ export const Base: Story = { utilCustomFunctions: { setState: () => {}, setErrorFieldMsg: () => {}, - clearAllErrorMsg: () => {}, + clearAllErrorMsg: (state) => state, setErrorMsg: () => {}, }, handleChange: () => {}, diff --git a/ui/src/components/ControlWrapper/test/ControlWrapper.test.tsx b/ui/src/components/ControlWrapper/test/ControlWrapper.test.tsx index 0c4a3569d..327f7ad53 100644 --- a/ui/src/components/ControlWrapper/test/ControlWrapper.test.tsx +++ b/ui/src/components/ControlWrapper/test/ControlWrapper.test.tsx @@ -10,7 +10,7 @@ const renderControlWrapper = (props: Partial) => { utilCustomFunctions: { setState: () => {}, setErrorFieldMsg: () => {}, - clearAllErrorMsg: () => {}, + clearAllErrorMsg: (state) => state, setErrorMsg: () => {}, }, handleChange: () => {}, diff --git a/ui/src/components/CustomControl/CustomControl.tsx b/ui/src/components/CustomControl/CustomControl.tsx index 810c7cb63..8ac3660d6 100644 --- a/ui/src/components/CustomControl/CustomControl.tsx +++ b/ui/src/components/CustomControl/CustomControl.tsx @@ -4,30 +4,12 @@ import { getUnifiedConfigs } from '../../util/util'; import { getBuildDirPath } from '../../util/script'; import { AcceptableFormValueOrNullish } from '../../types/components/shareableTypes'; import { UtilBaseForm } from '../../types/components/BaseFormTypes'; -import { GlobalConfig } from '../../types/globalConfig/globalConfig'; -import { Mode } from '../../constants/modes'; +import { invariant } from '../../util/invariant'; +import { CustomControlConstructor } from './CustomControlBase'; +import { ControlData } from './CustomControl.types'; -interface IData { - value: AcceptableFormValueOrNullish; - mode: Mode; - serviceName: string; -} - -interface ICustomCompClass { - new ( - config: GlobalConfig, - el: HTMLElement | undefined, - data: IData, - setValue: (field: string, newValue: AcceptableFormValueOrNullish) => void, - util: UtilBaseForm - ): { - render: () => void; - validation?: (submittedField: string, submittedValue: string) => void; - }; -} - -interface ICustomCompProps { - data: IData; +interface Props { + data: ControlData; field: string; handleChange: (field: string, newValue: AcceptableFormValueOrNullish) => void; controlOptions: { src: string; type: string }; @@ -38,29 +20,32 @@ interface ICustomCompProps { utilCustomFunctions: UtilBaseForm; } -interface ICustomCompState { +interface State { loading: boolean; } -class CustomControl extends React.Component { +class CustomControl extends React.Component { static loadCustomControl = ( module: string, type: string, appName: string - ): Promise => + ): Promise => new Promise((resolve) => { if (type === 'external') { import(/* webpackIgnore: true */ `${getBuildDirPath()}/custom/${module}.js`).then( - (external) => { - const Control = external.default; + async (external) => { + const Control = external.default as CustomControlConstructor; resolve(Control); } ); } else { // @ts-expect-error typeof __non_webpack_require__ is not known during bundle - __non_webpack_require__([`app/${appName}/js/build/custom/${module}`], (Control) => { - resolve(Control); - }); + __non_webpack_require__( + [`app/${appName}/js/build/custom/${module}`], + (Control: CustomControlConstructor) => { + resolve(Control); + } + ); } }); @@ -68,7 +53,7 @@ class CustomControl extends React.Component el?: HTMLElement; - constructor(props: ICustomCompProps) { + constructor(props: Props) { super(props); this.state = { loading: true, @@ -85,6 +70,7 @@ class CustomControl extends React.Component this.props.controlOptions.type, appName ).then((Control) => { + invariant(this.el !== undefined, 'Element should be defined'); const customControl = new Control( globalConfig, this.el, @@ -101,7 +87,7 @@ class CustomControl extends React.Component }); } - shouldComponentUpdate(_nextProps: ICustomCompProps, nextState: ICustomCompState) { + shouldComponentUpdate(_nextProps: Props, nextState: State) { if (!nextState.loading && this.shouldRender) { this.shouldRender = false; return true; diff --git a/ui/src/components/CustomControl/CustomControl.types.ts b/ui/src/components/CustomControl/CustomControl.types.ts new file mode 100644 index 000000000..f9d246d8e --- /dev/null +++ b/ui/src/components/CustomControl/CustomControl.types.ts @@ -0,0 +1,8 @@ +import { AcceptableFormValueOrNullish } from '../../types/components/shareableTypes'; +import { Mode } from '../../constants/modes'; + +export interface ControlData { + value: AcceptableFormValueOrNullish; + mode: Mode; + serviceName: string; +} diff --git a/ui/src/components/CustomControl/CustomControlBase.ts b/ui/src/components/CustomControl/CustomControlBase.ts new file mode 100644 index 000000000..b86e84c3f --- /dev/null +++ b/ui/src/components/CustomControl/CustomControlBase.ts @@ -0,0 +1,43 @@ +import { GlobalConfig } from '../../types/globalConfig/globalConfig'; +import { UtilBaseForm } from '../../types/components/BaseFormTypes'; +import { AcceptableFormValueOrNullish } from '../../types/components/shareableTypes'; +import { ControlData } from './CustomControl.types'; + +type ValueSetter = (newValue: AcceptableFormValueOrNullish) => void; + +export type CustomControlInstance = + InstanceType; + +export type CustomControlConstructor< + T extends typeof CustomControlBase = typeof CustomControlBase +> = new (...args: ConstructorParameters) => CustomControlInstance; + +export abstract class CustomControlBase { + protected globalConfig: GlobalConfig; + + protected el: HTMLElement; + + protected data: ControlData; + + protected setValue: ValueSetter; + + protected util: UtilBaseForm; + + constructor( + globalConfig: GlobalConfig, + el: HTMLElement, + data: ControlData, + setValue: ValueSetter, + util: UtilBaseForm + ) { + this.globalConfig = globalConfig; + this.el = el; + this.data = data; + this.setValue = setValue; + this.util = util; + } + + abstract render(): void; + + validation?(field: string, value: ControlData['value']): string | undefined; +} diff --git a/ui/src/components/CustomTab/CustomTab.tsx b/ui/src/components/CustomTab/CustomTab.tsx index f84902298..6a4bd0aa5 100644 --- a/ui/src/components/CustomTab/CustomTab.tsx +++ b/ui/src/components/CustomTab/CustomTab.tsx @@ -1,22 +1,14 @@ import React, { useEffect, useRef, useState } from 'react'; import { _ } from '@splunk/ui-utils/i18n'; -import { z } from 'zod'; import { getUnifiedConfigs } from '../../util/util'; import { getBuildDirPath } from '../../util/script'; -import { TabSchema } from '../../types/globalConfig/pages'; - -type Tab = z.infer; +import { CustomTabConstructor } from './CustomTabBase'; +import { Tab } from './CustomTab.types'; interface CustomTabProps { tab: Tab; } -interface ICustomTabClass { - new (tab: Tab, ref: HTMLDivElement): { - render: () => void; - }; -} - const CustomTab: React.FC = ({ tab }) => { const [loading, setLoading] = useState(true); const divRef = useRef(null); @@ -24,7 +16,7 @@ const CustomTab: React.FC = ({ tab }) => { const globalConfig = getUnifiedConfigs(); const appName = globalConfig.meta.name; - const loadCustomTab = (): Promise => + const loadCustomTab = (): Promise => new Promise((resolve) => { if (tab.customTab?.type === 'external') { import( @@ -37,7 +29,7 @@ const CustomTab: React.FC = ({ tab }) => { // @ts-expect-error should be exported to other js module and imported here __non_webpack_require__( [`app/${appName}/js/build/custom/${tab.customTab?.src}`], - (Control: ICustomTabClass) => resolve(Control) + (Control: CustomTabConstructor) => resolve(Control) ); } }); diff --git a/ui/src/components/CustomTab/CustomTab.types.ts b/ui/src/components/CustomTab/CustomTab.types.ts new file mode 100644 index 000000000..ebbd62e95 --- /dev/null +++ b/ui/src/components/CustomTab/CustomTab.types.ts @@ -0,0 +1,4 @@ +import { z } from 'zod'; +import { TabSchema } from '../../types/globalConfig/pages'; + +export type Tab = z.infer; diff --git a/ui/src/components/CustomTab/CustomTabBase.ts b/ui/src/components/CustomTab/CustomTabBase.ts new file mode 100644 index 000000000..ffd038a42 --- /dev/null +++ b/ui/src/components/CustomTab/CustomTabBase.ts @@ -0,0 +1,21 @@ +import { Tab } from './CustomTab.types'; + +export type CustomTabInstance = + InstanceType; + +export type CustomTabConstructor = new ( + ...args: ConstructorParameters +) => CustomTabInstance; + +export abstract class CustomTabBase { + protected tab: Tab; + + protected el: HTMLDivElement; + + constructor(tab: Tab, el: HTMLDivElement) { + this.tab = tab; + this.el = el; + } + + abstract render(): void; +} diff --git a/ui/src/components/DeleteModal/DeleteModal.tsx b/ui/src/components/DeleteModal/DeleteModal.tsx index c9487ecf0..7558438aa 100644 --- a/ui/src/components/DeleteModal/DeleteModal.tsx +++ b/ui/src/components/DeleteModal/DeleteModal.tsx @@ -17,7 +17,7 @@ const ModalWrapper = styled(Modal)` width: 800px; `; -interface DeleteModalProps { +export interface DeleteModalProps { page: StandardPages; handleRequestClose: () => void; serviceName: string; diff --git a/ui/src/components/EntityPage/EntityPage.tsx b/ui/src/components/EntityPage/EntityPage.tsx index dff3b1cd3..dd1b88fe2 100644 --- a/ui/src/components/EntityPage/EntityPage.tsx +++ b/ui/src/components/EntityPage/EntityPage.tsx @@ -17,7 +17,7 @@ import { StandardPages } from '../../types/components/shareableTypes'; import PageContext from '../../context/PageContext'; import { UCCButton } from '../UCCButton/UCCButton'; -interface EntityPageProps { +export interface EntityPageProps { handleRequestClose: () => void; serviceName: string; mode: Mode; diff --git a/ui/src/components/TextAreaComponent/TextAreaComponent.tsx b/ui/src/components/TextAreaComponent/TextAreaComponent.tsx index ee2b2f24b..0b6914624 100644 --- a/ui/src/components/TextAreaComponent/TextAreaComponent.tsx +++ b/ui/src/components/TextAreaComponent/TextAreaComponent.tsx @@ -6,7 +6,7 @@ const TextWrapper = styled(TextArea)` width: 320px !important; `; -interface TextAreaComponentProps { +export interface TextAreaComponentProps { id?: string; value: string | number; handleChange: (field: string, value: string) => void; diff --git a/ui/src/components/TextComponent/TextComponent.tsx b/ui/src/components/TextComponent/TextComponent.tsx index 70e63e8f0..602a74f75 100755 --- a/ui/src/components/TextComponent/TextComponent.tsx +++ b/ui/src/components/TextComponent/TextComponent.tsx @@ -6,7 +6,7 @@ const TextWrapper = styled(Text)` width: 320px !important; `; -interface TextComponentProps { +export interface TextComponentProps { // Number is expected if provided number in globalConfig.json instead of a string. value: string | number; handleChange: (field: string, value: string | number) => void; diff --git a/ui/src/publicApi.ts b/ui/src/publicApi.ts new file mode 100644 index 000000000..f9fb8442e --- /dev/null +++ b/ui/src/publicApi.ts @@ -0,0 +1,11 @@ +export { Mode } from './constants/modes'; + +export { GlobalConfig } from './types/globalConfig/globalConfig'; + +export { BaseFormState } from './types/components/BaseFormTypes'; + +export { CustomHookClass } from './types/components/CustomHookClass'; + +export { CustomControlBase } from './components/CustomControl/CustomControlBase'; + +export { CustomTabBase } from './components/CustomTab/CustomTabBase'; diff --git a/ui/src/types/components/BaseFormTypes.ts b/ui/src/types/components/BaseFormTypes.ts index f12af7687..1a727a4f4 100644 --- a/ui/src/types/components/BaseFormTypes.ts +++ b/ui/src/types/components/BaseFormTypes.ts @@ -17,7 +17,6 @@ import { TextAreaEntity, TextEntity, } from '../globalConfig/entities'; -import { GlobalConfig } from '../globalConfig/globalConfig'; import { PageContextProviderType } from '../../context/PageContext'; export type CurrentBaseFormInput = @@ -101,9 +100,9 @@ export interface SingleSelectEntityType { } export interface UtilBaseForm { - setState: (callback: (prevState: BaseFormState) => void) => void; + setState: (callback: (prevState: BaseFormState) => BaseFormState) => void; setErrorFieldMsg: (field: string, msg: string) => void; - clearAllErrorMsg: (State: BaseFormState) => unknown; + clearAllErrorMsg: (state: BaseFormState) => BaseFormState; setErrorMsg: (msg: string) => void; } @@ -131,20 +130,6 @@ export interface OauthConfiguration { authEndpointAccessTokenType: string | null; } -export interface CustomHook { - onCreate?: () => void; - onRender?: () => void; - onEditLoad?: () => void; - onSaveSuccess?: () => void; - onSaveFail?: () => void; - onChange?: ( - field: string, - targetValue: AcceptableFormValueOrNullish, - tempState: BaseFormState - ) => void; - onSave?: (datadict?: Record) => boolean; -} - export type AnyEntity = z.TypeOf | z.TypeOf; export type EntitiesAllowingModifications = @@ -172,30 +157,3 @@ export interface ChangeRecord { value?: { $set: AcceptableFormValueOrNullish }; dependencyValues?: { $set: Record }; } - -export interface CustomHookClass { - new ( - config: GlobalConfig, - serviceName: string, - state: BaseFormState, - mode: string, - util: { - setState: (callback: (prevState: BaseFormState) => void) => void; - setErrorFieldMsg: (field: string, msg: string) => void; - clearAllErrorMsg: (State: BaseFormState) => unknown; - setErrorMsg: (msg: string) => void; - } - ): { - onCreate?: () => void; - onRender?: () => void; - onEditLoad?: () => void; - onSaveSuccess?: () => void; - onSaveFail?: () => void; - onChange?: ( - field: string, - targetValue: AcceptableFormValueOrNullish, - tempState: BaseFormState - ) => void; - onSave?: (datadict?: Record) => boolean; - }; -} diff --git a/ui/src/types/components/CustomHookClass.ts b/ui/src/types/components/CustomHookClass.ts new file mode 100644 index 000000000..f8ff8abb7 --- /dev/null +++ b/ui/src/types/components/CustomHookClass.ts @@ -0,0 +1,61 @@ +import { GlobalConfig } from '../globalConfig/globalConfig'; +import { Mode } from '../../constants/modes'; +import { AcceptableFormValueOrNullish } from './shareableTypes'; +import { BaseFormState, UtilBaseForm } from './BaseFormTypes'; + +export type CustomHookInstance = + InstanceType; + +export type CustomHookConstructor = new ( + ...args: ConstructorParameters +) => CustomHookInstance; + +export abstract class CustomHookClass { + protected globalConfig: GlobalConfig; + + protected serviceName: string; + + protected state: BaseFormState; + + protected mode: Mode; + + protected util: UtilBaseForm; + + protected groupName?: string; + + constructor( + globalConfig: GlobalConfig, + serviceName: string, + state: BaseFormState, + mode: Mode, + util: UtilBaseForm, + groupName?: string + ) { + this.globalConfig = globalConfig; + this.serviceName = serviceName; + this.state = state; + this.mode = mode; + this.util = util; + this.groupName = groupName; + } + + onCreate?(): void; + + onRender?(): void; + + onChange?( + field: string, + targetValue: AcceptableFormValueOrNullish, + tempState: BaseFormState + ): void; + + onEditLoad?(): void; + + onSave?(datadict?: Record): Promise; + + onSave?(datadict?: Record): boolean; + + onSaveSuccess?(): void; + + onSaveFail?(): void; +} diff --git a/ui/tsconfig.lib.json b/ui/tsconfig.lib.json new file mode 100644 index 000000000..4e16def13 --- /dev/null +++ b/ui/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "declarationDir": "./dist/lib" + }, + "include": [ + "src/publicApi.ts" + ] +}