From 4bf38c8238121f615ae6c79482810c0028a646d8 Mon Sep 17 00:00:00 2001 From: Alejandro Visiedo Date: Wed, 20 Sep 2023 09:53:09 +0200 Subject: [PATCH] WIP Refactor the commits Signed-off-by: Alejandro Visiedo --- api | 2 +- docs/98-tips.md | 56 +++- package-lock.json | 94 +++++++ package.json | 1 + src/Api/api.ts | 212 +++++++------- src/App.tsx | 4 + src/AppContext.tsx | 54 +++- src/AppEntry.tsx | 41 ++- src/Routes/DefaultPage/DefaultPage.scss | 4 - src/Routes/DefaultPage/DefaultPage.tsx | 50 ++-- .../DomainOverview/DomainOverview.test.tsx | 40 --- .../DomainOverview/DomainOverview.tsx | 68 ----- .../PagePreparation/PagePreparation.scss | 8 - .../PagePreparation/PagePreparation.tsx | 194 +++++++------ .../Components/PageReview/PageReview.tsx | 170 ++++++++--- .../PageServiceDetails/PageServiceDetails.tsx | 109 ++++++-- .../PageServiceRegistration.tsx | 127 ++++++--- .../VerifyRegistry.scss} | 0 .../VerifyRegistry/VerifyRegistry.test.tsx | 10 + .../VerifyRegistry/VerifyRegistry.tsx | 263 ++++++++++++++++++ src/Routes/WizardPage/WizardPage.tsx | 215 +++++++++++--- 21 files changed, 1232 insertions(+), 490 deletions(-) delete mode 100644 src/Routes/WizardPage/Components/DomainOverview/DomainOverview.test.tsx delete mode 100644 src/Routes/WizardPage/Components/DomainOverview/DomainOverview.tsx rename src/Routes/WizardPage/Components/{DomainOverview/DomainOverview.scss => VerifyRegistry/VerifyRegistry.scss} (100%) create mode 100644 src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.test.tsx create mode 100644 src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.tsx diff --git a/api b/api index ac0c464..69afcd3 160000 --- a/api +++ b/api @@ -1 +1 @@ -Subproject commit ac0c464a6e650084f0d0f7bb73db6f47474f9b65 +Subproject commit 69afcd340496e6a47a08b2e45b5811bc5bcf62c8 diff --git a/docs/98-tips.md b/docs/98-tips.md index d9c7583..0c709fd 100644 --- a/docs/98-tips.md +++ b/docs/98-tips.md @@ -1,5 +1,19 @@ # Good practices +## Document your components + +Document, document, document. Today we are confident about what we are +coding, tomorrow maybe we have forgotten everything about that new, +fancy and awesome component. It is for you, for your team and the +community. A component that is well documented, can be enhanced by +other team mates or the community. + +Use TSDoc to add documentation to your components. + +See: https://tsdoc.org/ + +TODO Add examples. + ## Break-Down the page and components This is basically **divide and conquer** principle; smaller things are easier @@ -28,7 +42,9 @@ For instance: import './sample-component.scss'; import React from 'react'; -interface SampleComponentProp { +/* SampleComponent component */ + +interface SampleComponentProps { name: string; description: string; } @@ -39,12 +55,10 @@ interface SampleComponentProp { * * @param props the props given by the smart component. */ -const SampleComponent: React.FC = (props) => { +const SampleComponent = (props: SampleComponentProps) => { return {props.children} ; }; -SampleComponent.displayName = 'SampleComponent'; - export default SampleComponent; ``` @@ -70,7 +84,7 @@ interface SampleComponentState { } /* React component */ -const SampleComponent: React.FC = (props) => { +const SampleComponent = (props: SampleComponentProp) => { /* Define the states */ const [state, setState] useState({name: props.name, description: props.description}); @@ -106,7 +120,7 @@ interface MyState { count: number; } -const MyComponent: React.FC = (props) => { +const MyComponent = (props: MyComponentProps) => { const [state, setState] = useState({count: 0}); return MyComponent; }; @@ -133,6 +147,36 @@ class MyComponent extends React.Component { } ``` +## Forward properties when no levels or not many levels + +The initial approach would be to define for a new component the interface +with the values to be passed as properties; this include values used and `events` +that will receive the parent component. This is the first immediate mechanism +to communicate with the parent component. + +```typescript +interface MyComponentProps { + value: string; + onChange: (value: string) => void; +} + +const MyComponent = (props: MyComponentProps) => { + const [state, setState] = useState(props.value); + const onChange: (value: string) => { + setState(value); + props.onChange(state); + }; + return ( + + ); +}; +``` + +In the above case, MyComponent receive the value to be displayed into +the `input` component, and when it is changed, the new value is send +to the parent component by calling the onChange callback provided by +the parent component. + ## Use context when necessary The most immediate way to pass information between the components diff --git a/package-lock.json b/package-lock.json index e5d4aa1..12e0b4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "babel-jest": "27.0.5", "babel-plugin-transform-imports": "^2.0.0", "eslint": "8.18.0", + "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unused-imports": "^2.0.0", "identity-obj-proxy": "3.0.0", "jest": "27.0.5", @@ -3128,6 +3129,37 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@nestjs/axios": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", @@ -9017,6 +9049,16 @@ "node": ">=4.0.0" } }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz", + "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, "node_modules/eslint-plugin-unused-imports": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", @@ -14380,6 +14422,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -23320,6 +23368,36 @@ "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", "dev": true }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, "@nestjs/axios": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", @@ -27949,6 +28027,16 @@ "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true }, + "eslint-plugin-tsdoc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz", + "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, "eslint-plugin-unused-imports": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", @@ -31812,6 +31900,12 @@ } } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index d62098c..03bbc73 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "babel-jest": "27.0.5", "babel-plugin-transform-imports": "^2.0.0", "eslint": "8.18.0", + "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unused-imports": "^2.0.0", "identity-obj-proxy": "3.0.0", "jest": "27.0.5", diff --git a/src/Api/api.ts b/src/Api/api.ts index e7f1441..fd04316 100644 --- a/src/Api/api.ts +++ b/src/Api/api.ts @@ -970,56 +970,6 @@ export const ActionsApiAxiosParamCreator = function (configuration?: Configurati localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; localVarRequestOptions.data = serializeDataIfNeeded(updateDomainAgentRequest, localVarRequestOptions, configuration); - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * Update the rhel-idm domain information. - * @summary Update domain information by user. - * @param {string} uuid The uuid that identify the domain. - * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. - * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - updateDomainUser: async ( - uuid: string, - updateDomainUserRequest: UpdateDomainUserRequest, - xRhInsightsRequestId?: string, - options: AxiosRequestConfig = {} - ): Promise => { - // verify required parameter 'uuid' is not null or undefined - assertParamExists('updateDomainUser', 'uuid', uuid); - // verify required parameter 'updateDomainUserRequest' is not null or undefined - assertParamExists('updateDomainUser', 'updateDomainUserRequest', updateDomainUserRequest); - const localVarPath = `/domains/{uuid}`.replace(`{${'uuid'}}`, encodeURIComponent(String(uuid))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options }; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication x-rh-identity required - await setApiKeyToObject(localVarHeaderParameter, 'X-Rh-Identity', configuration); - - if (xRhInsightsRequestId != null) { - localVarHeaderParameter['X-Rh-Insights-Request-Id'] = String(xRhInsightsRequestId); - } - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; - localVarRequestOptions.data = serializeDataIfNeeded(updateDomainUserRequest, localVarRequestOptions, configuration); - return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -1081,24 +1031,6 @@ export const ActionsApiFp = function (configuration?: Configuration) { ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * Update the rhel-idm domain information. - * @summary Update domain information by user. - * @param {string} uuid The uuid that identify the domain. - * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. - * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async updateDomainUser( - uuid: string, - updateDomainUserRequest: UpdateDomainUserRequest, - xRhInsightsRequestId?: string, - options?: AxiosRequestConfig - ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, }; }; @@ -1149,23 +1081,6 @@ export const ActionsApiFactory = function (configuration?: Configuration, basePa .updateDomainAgent(uuid, xRhIdmVersion, updateDomainAgentRequest, xRhInsightsRequestId, options) .then((request) => request(axios, basePath)); }, - /** - * Update the rhel-idm domain information. - * @summary Update domain information by user. - * @param {string} uuid The uuid that identify the domain. - * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. - * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - updateDomainUser( - uuid: string, - updateDomainUserRequest: UpdateDomainUserRequest, - xRhInsightsRequestId?: string, - options?: any - ): AxiosPromise { - return localVarFp.updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options).then((request) => request(axios, basePath)); - }, }; }; @@ -1215,27 +1130,6 @@ export class ActionsApi extends BaseAPI { .updateDomainAgent(uuid, xRhIdmVersion, updateDomainAgentRequest, xRhInsightsRequestId, options) .then((request) => request(this.axios, this.basePath)); } - - /** - * Update the rhel-idm domain information. - * @summary Update domain information by user. - * @param {string} uuid The uuid that identify the domain. - * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. - * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof ActionsApi - */ - public updateDomainUser( - uuid: string, - updateDomainUserRequest: UpdateDomainUserRequest, - xRhInsightsRequestId?: string, - options?: AxiosRequestConfig - ) { - return ActionsApiFp(this.configuration) - .updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options) - .then((request) => request(this.axios, this.basePath)); - } } /** @@ -1510,6 +1404,56 @@ export const ResourcesApiAxiosParamCreator = function (configuration?: Configura localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; localVarRequestOptions.data = serializeDataIfNeeded(registerDomainRequest, localVarRequestOptions, configuration); + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Update the rhel-idm domain information. + * @summary Update domain information by user. + * @param {string} uuid The uuid that identify the domain. + * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. + * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateDomainUser: async ( + uuid: string, + updateDomainUserRequest: UpdateDomainUserRequest, + xRhInsightsRequestId?: string, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'uuid' is not null or undefined + assertParamExists('updateDomainUser', 'uuid', uuid); + // verify required parameter 'updateDomainUserRequest' is not null or undefined + assertParamExists('updateDomainUser', 'updateDomainUserRequest', updateDomainUserRequest); + const localVarPath = `/domains/{uuid}`.replace(`{${'uuid'}}`, encodeURIComponent(String(uuid))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication x-rh-identity required + await setApiKeyToObject(localVarHeaderParameter, 'X-Rh-Identity', configuration); + + if (xRhInsightsRequestId != null) { + localVarHeaderParameter['X-Rh-Insights-Request-Id'] = String(xRhInsightsRequestId); + } + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(updateDomainUserRequest, localVarRequestOptions, configuration); + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -1631,6 +1575,24 @@ export const ResourcesApiFp = function (configuration?: Configuration) { ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Update the rhel-idm domain information. + * @summary Update domain information by user. + * @param {string} uuid The uuid that identify the domain. + * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. + * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateDomainUser( + uuid: string, + updateDomainUserRequest: UpdateDomainUserRequest, + xRhInsightsRequestId?: string, + options?: AxiosRequestConfig + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, }; }; @@ -1717,6 +1679,23 @@ export const ResourcesApiFactory = function (configuration?: Configuration, base .registerDomain(xRhIdmRegistrationToken, xRhIdmVersion, registerDomainRequest, xRhInsightsRequestId, options) .then((request) => request(axios, basePath)); }, + /** + * Update the rhel-idm domain information. + * @summary Update domain information by user. + * @param {string} uuid The uuid that identify the domain. + * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. + * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateDomainUser( + uuid: string, + updateDomainUserRequest: UpdateDomainUserRequest, + xRhInsightsRequestId?: string, + options?: any + ): AxiosPromise { + return localVarFp.updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options).then((request) => request(axios, basePath)); + }, }; }; @@ -1824,4 +1803,25 @@ export class ResourcesApi extends BaseAPI { .registerDomain(xRhIdmRegistrationToken, xRhIdmVersion, registerDomainRequest, xRhInsightsRequestId, options) .then((request) => request(this.axios, this.basePath)); } + + /** + * Update the rhel-idm domain information. + * @summary Update domain information by user. + * @param {string} uuid The uuid that identify the domain. + * @param {UpdateDomainUserRequest} updateDomainUserRequest Information for an IPA domain so it is updated from the ipa-hcc agent. + * @param {string} [xRhInsightsRequestId] Request id for distributed tracing. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ResourcesApi + */ + public updateDomainUser( + uuid: string, + updateDomainUserRequest: UpdateDomainUserRequest, + xRhInsightsRequestId?: string, + options?: AxiosRequestConfig + ) { + return ResourcesApiFp(this.configuration) + .updateDomainUser(uuid, updateDomainUserRequest, xRhInsightsRequestId, options) + .then((request) => request(this.axios, this.basePath)); + } } diff --git a/src/App.tsx b/src/App.tsx index fd84536..ee96ad4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,10 @@ import NotificationsPortal from '@redhat-cloud-services/frontend-components-noti import { notificationsReducer } from '@redhat-cloud-services/frontend-components-notifications/redux'; import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'; +/** + * Entry point for our appication. It provides the notification portal, + * and the routes for our application. + */ const App = () => { const navigate = useNavigate(); const { on } = useChrome(); diff --git a/src/AppContext.tsx b/src/AppContext.tsx index ed5c41b..7e9291d 100644 --- a/src/AppContext.tsx +++ b/src/AppContext.tsx @@ -1,20 +1,62 @@ import { createContext } from 'react'; import { Domain } from './Api'; +import { VerifyState } from './Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry'; +/** + * It represents the application context so common events and properties + * are shared for many components, making their values accessible. + * @public + */ export interface IAppContext { + /** Represent the current list of domains to be displayed in the listDomains view. */ domains: Domain[]; + /** Callback to set the value of `domains`. */ setDomains: (domains: Domain[]) => void; - // wizardDomain?: Domain; - // setWizardDomain: (domain?: Domain) => void; + /** Encapsulates the context related with the wizard. */ + wizard: { + /** Retrieve the current token, required to register a domain. */ + getToken: () => string; + /** Set the value of the token. */ + setToken: (value: string) => void; + /** Retrieve the value of the registered status which is updated once + * the user has registered the domain by using ipa-hcc tool. */ + getRegisteredStatus: () => VerifyState; + /** Setter for the registered status. */ + setRegisteredStatus: (value: VerifyState) => void; + /** Get the ephemeral domain state that manage the wizard. */ + getDomain: () => Domain; + /** Set the ephemeral domain information. */ + setDomain: (value: Domain) => void; + }; } +/** + * Represent the application context. + * @public + */ export const AppContext = createContext({ domains: [], setDomains: (domains: Domain[]) => { throw new Error('Function "setDomains" not implemented: domains=' + domains); }, - // wizardDomain: undefined, - // setWizardDomain: (domain?: Domain) => { - // throw new Error('Function "setWizardDomain" not implemented: domain=' + domain); - // }, + wizard: { + getToken: (): string => { + return ''; + }, + setToken: (value: string) => { + throw new Error('Function "setToken" not implemented: value=' + value); + }, + getRegisteredStatus: (): VerifyState => { + return 'initial'; + }, + setRegisteredStatus: (value: VerifyState) => { + throw new Error('Function "setRegisteredStatus" not implemented: value=' + value); + }, + getDomain: (): Domain => { + return {} as Domain; + }, + setDomain: (value: Domain) => { + throw new Error('Function "setDomain" not implemented: value=' + value); + }, + }, }); diff --git a/src/AppEntry.tsx b/src/AppEntry.tsx index 8bf2bf4..557347b 100644 --- a/src/AppEntry.tsx +++ b/src/AppEntry.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { Provider } from 'react-redux'; import { init } from './store'; @@ -7,16 +7,53 @@ import { getBaseName } from '@redhat-cloud-services/frontend-components-utilitie import logger from 'redux-logger'; import { AppContext } from './AppContext'; import { Domain } from './Api'; +import { VerifyState } from './Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry'; const AppEntry = () => { + const appContext = useContext(AppContext); const [domains, setDomains] = useState([]); + const [wizardToken, setWizardToken] = useState(''); + const [wizardDomain, setWizardDomain] = useState({} as Domain); + const [wizardRegisterStatus, setWizardRegisterStatus] = useState('initial'); const cbSetDomains = (domains: Domain[]) => { + appContext.domains = domains; setDomains(domains); }; + const cbGetWizardToken = (): string => { + return wizardToken; + }; + const cbSetWizardToken = (value: string) => { + setWizardToken(value); + }; + const cbGetWizardDomain = (): Domain => { + return wizardDomain; + }; + const cbSetWizardDomain = (value: Domain) => { + setWizardDomain(value); + }; + const cbGetRegisterStatus = (): VerifyState => { + return wizardRegisterStatus; + }; + const cbSetRegisterStatus = (value: VerifyState) => { + setWizardRegisterStatus(value); + }; return ( - + diff --git a/src/Routes/DefaultPage/DefaultPage.scss b/src/Routes/DefaultPage/DefaultPage.scss index 5624387..acd9576 100644 --- a/src/Routes/DefaultPage/DefaultPage.scss +++ b/src/Routes/DefaultPage/DefaultPage.scss @@ -1,5 +1 @@ @import '~@redhat-cloud-services/frontend-components-utilities/styles/variables'; - -.empty-state-body-content { - margin-bottom: $ins-margin; -} diff --git a/src/Routes/DefaultPage/DefaultPage.tsx b/src/Routes/DefaultPage/DefaultPage.tsx index bb41404..3ba5006 100644 --- a/src/Routes/DefaultPage/DefaultPage.tsx +++ b/src/Routes/DefaultPage/DefaultPage.tsx @@ -27,7 +27,7 @@ import './DefaultPage.scss'; import Section from '@redhat-cloud-services/frontend-components/Section'; import { Domain, ResourcesApiFactory } from '../../Api/api'; import { DomainList } from '../../Components/DomainList/DomainList'; -import { AppContext } from '../../AppContext'; +import { AppContext, IAppContext } from '../../AppContext'; // const SampleComponent = lazy(() => import('../../Components/SampleComponent/sample-component')); @@ -51,9 +51,15 @@ const Header = () => { }; const EmptyContent = () => { + // FIXME Update this link in the future + const linkLearnMoreAbout = 'https://access.redhat.com/articles/1586893'; const navigate = useNavigate(); + const appContext = useContext(AppContext); const handleOpenWizard = () => { + appContext.wizard.setDomain({ domain_id: '', title: '', description: '' } as Domain); + appContext.wizard.setToken(''); + appContext.wizard.setRegisteredStatus('initial'); navigate('/domains/wizard', { replace: true }); }; @@ -62,21 +68,34 @@ const EmptyContent = () => {
- - - No domains + <RegistryIcon className="pf-u-color-200" size="xl" /> + <Title headingLevel="h2" size="lg" className="pf-u-pt-sm"> + No direcotry and domain services registered - - To specify which existing access controls can be -
leveraged for hosts launched through the console, you -
must first register your domain(s). As part of that -
process you will be required to SSO into your server(s) -
and install packages via CLI. + + Use access controls from your existing IdM hosts in your cloud +
environment*. To get started, register a service. +
+ + + + + - - + + *Directory and domain services are currently available only for Red Hat IdM and IPA deployments.
@@ -134,6 +153,9 @@ const ListContent = () => { }, [page, perPage, offset]); const handleOpenWizard = () => { + appContext.wizard.setDomain({ domain_id: '', title: '', description: '' } as Domain); + appContext.wizard.setRegisteredStatus('initial'); + appContext.wizard.setToken(''); navigate('/domains/wizard', { replace: true }); }; @@ -205,10 +227,6 @@ const DefaultPage = () => { const [perPage] = useState(10); const [offset, setOffset] = useState(0); - useEffect(() => { - insights?.chrome?.appAction?.('default-page'); - }, []); - console.log('INFO:DefaultPage render'); useEffect(() => { diff --git a/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.test.tsx b/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.test.tsx deleted file mode 100644 index 5bdbd59..0000000 --- a/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import DomainOverview from './DomainOverview'; -import '@testing-library/jest-dom'; -import { Domain } from '../../../../Api'; - -test('expect sample-component to render children', () => { - const location = 'boston'; - const subscription_manager_id = '21ee4400-4bfc-11ee-ada9-482ae3863d30'; - const domain_demo: Domain = { - domain_name: 'mydomain.example', - domain_type: 'rhel-idm', - 'rhel-idm': { - ca_certs: [], - locations: [ - { - name: location, - description: 'cpd located at Boston', - }, - ], - realm_domains: ['mydomain.example'], - realm_name: 'MYDOMAIN.EXAMPLE', - servers: [ - { - location: location, - fqdn: 'server1.mydomain.example', - ca_server: true, - pkinit_server: true, - subscription_manager_id: subscription_manager_id, - hcc_enrollment_server: true, - hcc_update_server: true, - }, - ], - }, - }; - - render(); - expect(screen.getAllByRole('rowgroup')[1].children[0].children[0].textContent).toEqual(location); - expect(screen.getAllByRole('rowgroup')[1].children[0].children[1].textContent).toEqual(subscription_manager_id); -}); diff --git a/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.tsx b/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.tsx deleted file mode 100644 index cae5945..0000000 --- a/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import './DomainOverview.scss'; -import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -import { Fragment, useState } from 'react'; -import React from 'react'; - -import { Domain } from '../../../../Api/api'; - -export interface IColumnType { - key: string; - title: string; - width?: number; - render?: (columnd: IColumnType, item: T) => void; -} - -/** - * This represent the table header displayed for the DomainList. - * @returns The table header for the DomainList component. - */ -const DomainRhelIdmHead: React.FC = () => { - return ( - - - Name - UUID - - - ); -}; - -const DomainRhelIdmBody: React.FC<{ domain: Domain }> = (props) => { - const [domain] = useState(props.domain); - - return ( - - {domain['rhel-idm']?.servers.map((server) => { - return ( - - {server.location} - {server.subscription_manager_id} - - ); - })} - - ); -}; - -export interface DomainProps { - domain: Domain; -} - -export const DomainOverview: React.FC = (props) => { - const [domain] = useState(props.domain); - - return ( - <> - - {domain.domain_type == 'rhel-idm' && ( - <> - - - - )} - - - ); -}; - -export default DomainOverview; diff --git a/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.scss b/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.scss index 219c002..acd9576 100644 --- a/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.scss +++ b/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.scss @@ -1,9 +1 @@ @import '~@redhat-cloud-services/frontend-components-utilities/styles/variables'; - -.domain-type-select { - width: 50%; -} - -.domain-item-margin-left { - margin-left: 16px; -} diff --git a/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx b/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx index 511024a..d340d19 100644 --- a/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx +++ b/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx @@ -1,26 +1,81 @@ -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon'; -import { Button, ClipboardCopy, Form, FormGroup, Icon, Select, SelectOption, Stack, TextContent } from '@patternfly/react-core'; +import { Button, ClipboardCopy, Form, FormGroup, Icon, Select, SelectOption, TextContent, Title } from '@patternfly/react-core'; import './PagePreparation.scss'; +import { DomainType, ResourcesApiFactory } from '../../../../Api'; +import { AppContext, IAppContext } from '../../../../AppContext'; -const PagePreparation: React.FC = () => { - // TODO Update links - const firewallConfigurationLink = - 'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/using-and-configuring-firewalld_configuring-and-managing-networking'; - const cloudProviderConfigurationLink = - 'https://access.redhat.com/documentation/es-es/red_hat_subscription_management/2023/html-single/red_hat_cloud_access_reference_guide/index'; - const networkConfigurationLink = 'https://www.redhat.com/sysadmin/network-interface-linux'; - const installServerPackagesLink = 'https://freeipa.org/page/Quick_Start_Guide'; +/** Represent the properties for PagePreparation component. */ +interface PagePreparationProps { + token?: string; + onToken?: (token: string, domain_id: string, expiration: number) => void; +} + +/** + * This page provide information and links to prepare the rhel-idm + * servers before the user could proceed to the registration process. + * @param props provide the token value and the onToken event to + * notify when it is created. + * @returns Return the view to inform the user how to prepare for + * the domain registration. + * @public + * @see {@link PagePreparationProps} to know about the properties. + * @see {@link WizardPage} to know about the parent component. + */ +const PagePreparation = (props: PagePreparationProps) => { + // FIXME Update the target link when it is known + const installServerPackagesLink = 'https://duckduckgo.com/?q=freeipa+prerequisites'; + // FIXME Update the target link when it is known + const prerequisitesLink = 'https://www.google.com?q=rhel-idm+pre-requisites'; // States - const [isOpen, setIsOpen] = React.useState(false); + const [isOpen, setIsOpen] = useState(false); + const appContext = useContext(AppContext); + + const base_url = '/api/idmsvc/v1'; + const resources_api = ResourcesApiFactory(undefined, base_url, undefined); + + const token = appContext.wizard.getToken(); + const domain = appContext.wizard.getDomain(); + const domain_id = domain.domain_id ? domain.domain_id : ''; + + /** + * side effect to retrieve a token in the background. + * TODO When more than one type of domain, this callback will + * invoke from 'onRegisterDomainTypeSelect' event. + */ + useEffect(() => { + if (token != '' && domain_id != '') { + return; + } + // NOTE await and async cannot be used directly because EffectCallback cannot be a Promise + resources_api + .createDomainToken({ domain_type: 'rhel-idm' }, undefined, undefined) + .then((value) => { + appContext.wizard.setToken(value.data.domain_token); + const domain_id = value.data.domain_id; + const domain_name = 'My domain'; + const domain_type = value.data.domain_type; + const token = value.data.domain_token; + const expiration = value.data.expiration; + appContext.wizard.setDomain({ + domain_id: domain_id, + domain_name: domain_name, + domain_type: domain_type, + }); + props.onToken?.(token, domain_id, expiration); + }) + .catch((reason) => { + // FIXME handle the error here + console.log('error creating the token by createDomainToken: ' + reason); + }); + }, [token, domain_id]); - // hooks const onRegisterDomainTypeSelect = () => { - // TODO Not implemented - console.debug('onRegisterDomainTypeSelect in WizardPage'); + // TODO Not implemented, currently only support rhel-idm + console.debug('PagePreparation.onRegisterDomainTypeSelect in WizardPage'); return; }; @@ -30,29 +85,29 @@ const PagePreparation: React.FC = () => { const domainOptions = [ { - value: 'rhel-idm', - title: 'Red Hat Enterprise Linux IdM/IPA', + value: 'rhel-idm' as DomainType, + title: 'RHEL IdM (IPA)', }, ]; return ( - + <> + Preparation for your directory and domain service
{ - console.debug('onSubmit WizardPage' + String(value)); + console.debug('TODO onSubmit WizardPage' + String(value)); }} > - - - Only Red Hat Linux IdM/IPA are currently supported. + {' '} + Only RHEL IdM (IPA) are currently supported. } > @@ -63,7 +118,7 @@ const PagePreparation: React.FC = () => { onSelect={onRegisterDomainTypeSelect} // onOpenChange={(isOpen) => setIsOpen(isOpen)} onToggle={onToggleClick} - className="domain-type-select" + className="pf-u-w-100 pf-u-w-50-on-md pf-u-w-50-on-xl" > {domainOptions.map((option) => ( @@ -72,57 +127,10 @@ const PagePreparation: React.FC = () => { ))} - - - There are prerequisites that must be completed to create and use security for Red Hat Linux IdM/IPA. If any prerequisites are already in - place, please skip to the next step: - - -
- -
- -
- - 4. Verify wether or not the package is present on your server(st) with this command: - - dnf list installed ipa-hcc-server - - - If the package is not present on your server(s), follow these steps:{' '} + +
    +
  1. + Complete the{' '} +
  2. +
  3. + Verify whether or not the package is present on your server(s) with this command: - dnf copr enable copr.devel.redhat.com/cheimes/ipa-hcc && dnf install ipa-hcc-server + dnf list installed ipa-hcc-server - The package must be installed on at least one IPA server. For redundency, the package should be installed on two or more IPA servers, - possibly all IPA servers. - - + + If the package is not present on your server(s), follow these{' '} + + + dnf copr enable copr.devel.redhat.com/cheimes/ipa-hcc && dnf install ipa-hcc-server + + The package must be installed on at least one IPA server. For redundency, the package should be installed on two or more IPA servers, + possibly all IPA servers. + +
  4. +
-
+ ); }; diff --git a/src/Routes/WizardPage/Components/PageReview/PageReview.tsx b/src/Routes/WizardPage/Components/PageReview/PageReview.tsx index ca6a794..44eb541 100644 --- a/src/Routes/WizardPage/Components/PageReview/PageReview.tsx +++ b/src/Routes/WizardPage/Components/PageReview/PageReview.tsx @@ -1,21 +1,101 @@ -import React, { useState } from 'react'; +/** + * This library encapsulate the PageReview page for the wizard + * component. + * + * @example + * Basic usage + * ``` + * + * ``` + * + * @packageDocumentation + */ +import React from 'react'; -import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Switch } from '@patternfly/react-core'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Title } from '@patternfly/react-core'; import './PageReview.scss'; -import { Domain } from '../../../../Api/api'; -import DomainOverview from '../../Components/DomainOverview/DomainOverview'; +import { Domain, DomainIpaServer } from '../../../../Api/api'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -const PageReview: React.FC<{ data: Domain }> = (props) => { - const [isHostJoinEnabled, setIsHostJoinEnabled] = React.useState(true); - const [domain] = useState(props.data); +/** + * Encapsulate the table header for the list of + * rhel-idm ipa servers. + * @returns Return the table header render. + * @see {@link PageReviewIpaServers} about the parent component. + */ +const PageReviewIpaServersHead = () => { + return ( + + + Name + UUID + + + ); +}; + +/** + * Represents the propoerties for PageReviewIpaServersBody. + */ +interface PageReviewIpaServersProps { + /** The list of ipa servers associated to the rhel-idm. */ + servers?: DomainIpaServer[]; +} + +/** + * Represents the body which shows the IPA server list. + * @param props Contains the list of servers at `servers`. + * @returns the body for the list of servers to use in the + * `TableComposable` component. + * @see {@link PageReviewIpaServersProps} about the properties. + * @see {@link PageReviewIpa} about the parent component. + */ +const PageReviewIpaServersBody = (props: PageReviewIpaServersProps) => { + return ( + + {props.servers?.map((server) => { + return ( + + {server.fqdn} + {server.subscription_manager_id} + + ); + })} + + ); +}; - const onHostJoinEnabledChange = () => { - setIsHostJoinEnabled(!isHostJoinEnabled); - }; +/** + * Represent the table which list the IPA servers. + * @param props has the `servers` property which contains the + * list of servers that belongs to this rhel-idm domain. + * @returns the composable table with the header and body. + * @see {@link PageReviewIpaServersProps} about the properties. + * @see {@link PageReviewIpa} about the parent component. + */ +const PageReviewIpaServers = (props: PageReviewIpaServersProps) => { + return ( + <> + + + + + + ); +}; +/** + * This component represent the overview information to be + * presented for an rhel-idm domain service. + * @param props the `domain` property expect a 'rhel-idm' type. + * @returns the overview details for a rhel-idm domain service. + * @see {@link PageReviewProps} about the properties. + * @see {@link PageReview} about the parent component. + */ +const PageReviewIpa = (props: PageReviewProps) => { return ( - + <> = (props) => { }} > - Identity and access management solution - {domain.domain_type === 'rhel-idm' &&
RHEL IdM (IPA)
}
+ Service type + RHEL IdM (IPA)
- Name - {domain.title} + Kerberos realm + {props.domain['rhel-idm']?.realm_name} - Description - {domain.description} + Red Hat IdM/IPA servers + + + - DNS Domain/Servers - - {/* TODO Navigator panel */} - 1-3 of 3 v   <   > - + Service name + {props.domain.title} + + + Service description + {props.domain.description} - - Allow host domain join - - + Domain join on launch + + {props.domain.auto_enrollment_enabled ? 'Enable upon finishing registration' : 'Not enable upon finishing registration'}
-
+ + ); +}; + +/** + * Represent the properties for the PageReview component. + */ +interface PageReviewProps { + /** The ephemeral domain information, including the detailed + * information that the user have control about. */ + domain: Domain; +} + +/** + * It represents the Page review wizard, and it provide different view + * depending on the domain_type value. + * @param props provide the `domain` value to be rendered. + * @returns the render view for the domain overview. + * @see {@link PageReviewProps} to know about the properties. + * @see {@link WizardPage} about the parent component. + * @public + */ +const PageReview = (props: PageReviewProps) => { + return ( + <> + + Review + + {props.domain.domain_type === 'rhel-idm' && } + ); }; diff --git a/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx b/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx index 3b9e8cf..23a39da 100644 --- a/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx +++ b/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx @@ -1,18 +1,68 @@ import React, { useState } from 'react'; -import { Form, FormGroup, TextArea, Title, Tooltip } from '@patternfly/react-core'; +import { Form, FormGroup, Icon, TextArea, Title, Tooltip } from '@patternfly/react-core'; import { TextInput } from '@patternfly/react-core'; -import { Domain } from '../../../../Api/api'; -import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; import './PageServiceDetails.scss'; import { Switch } from '@patternfly/react-core'; -const PageServiceDetails: React.FC<{ data: Domain }> = (props) => { - const [data, setData] = useState(props.data); - const [isAutoEnrollmentEnabled, setIsAutoEnrollmentEnabled] = React.useState(true); +/** + * Represent the properties accepted by the PageServiceDetails + * component. + * @see {@link PageServiceDetails} + */ +interface PageServiceDetailsProps { + /** The title that represent the domain. */ + title?: string; + /** The long description for the domain. */ + description?: string; + /** Flag to enable / disable the auto enrollment feature. */ + autoEnrollmentEnabled?: boolean; + + /** Event fired when the title change. */ + onChangeTitle?: (value: string) => void; + /** Event fired when the description change. */ + onChangeDescription?: (value: string) => void; + /** Event fired when the switch for auto-enrollment change. */ + onChangeAutoEnrollment?: (value: boolean) => void; +} + +/** + * It provides fields to the user to customize the values such as + * the title, description and if the domain will be avialable for + * auto-enrollment. + * @param props the properties received from the parent component. + * @returns return the view for the Service Details wizard page. + * @public + * @see {@link PageServiceDetailsProps} about the properties. + * @see {@link WizardPage} to know about the parent component. + */ +const PageServiceDetails = (props: PageServiceDetailsProps) => { + const [title, setTitle] = useState(props.title ? props.title : ''); + const [description, setDescription] = useState(props.description ? props.description : ''); + const [isAutoEnrollmentEnabled, setIsAutoEnrollmentEnabled] = React.useState( + props.autoEnrollmentEnabled ? props.autoEnrollmentEnabled : false + ); const onChangeAutoEnrollment = (checked: boolean) => { setIsAutoEnrollmentEnabled(checked); + if (props.onChangeAutoEnrollment) { + props.onChangeAutoEnrollment(checked); + } + }; + + const onChangeTitle = (value: string, event: React.FormEvent) => { + setTitle(value); + if (props.onChangeTitle) { + props.onChangeTitle(value); + } + }; + + const onChangeDescription = (value: string, event: React.ChangeEvent) => { + setDescription(value); + if (props.onChangeDescription) { + props.onChangeDescription(value); + } }; const autoEnrollmentTooltipContent = isAutoEnrollmentEnabled @@ -20,15 +70,11 @@ const PageServiceDetails: React.FC<{ data: Domain }> = (props) => { : 'Disabling the option leaves the service registration intact, but does not make it available for the "Domain join on launch" feature within Image Builder. It can be enabled later in the "Register Directory and Domain Service" view.'; return ( - -
{ - console.debug('onSubmit WizardPage' + String(value)); - }} - > + <> + e.preventDefault()}> Service Details - + + className="pf-u-w-100 pf-u-w-50-on-md pf-u-w-50-on-xl" + value={description} + onChange={onChangeDescription} + /> - Domain join on launch + Domain join on launch{' '} + + + + + } > - - - +
-
+ ); }; diff --git a/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx b/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx index 34e640a..231580d 100644 --- a/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx +++ b/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx @@ -1,59 +1,96 @@ -import React from 'react'; -// import { useDispatch } from 'react-redux'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; -import { Alert, Button, ClipboardCopy, Form, FormGroup, TextContent } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { Alert, Button, ClipboardCopy, Flex, FlexItem, Form, TextContent, Title } from '@patternfly/react-core'; import './PageServiceRegistration.scss'; -import { Domain } from '../../../../Api/api'; +import VerifyRegistry, { VerifyState } from '../VerifyRegistry/VerifyRegistry'; +import { Domain } from '../../../../Api'; +/** + * Represents the properties accepted by the @{link PageServiceRegistration} component. + * @see @{link VerifyState} about the different states. + */ interface PageServiceRegistrationProp { - data?: Domain; - token?: string; + /** The uuid for the requested token. */ + uuid: string; + /** The token requested for the operation. */ + token: string; + /** Event fired when the registration state changes. */ + onVerify?: (value: VerifyState, data?: Domain) => void; } -const PageServiceRegistration: React.FC = (props) => { - // FIXME Delete this - const demoToken = 'F4ZWgmhUxcw.d2iqKLHa8281CM_1aknGLsBRFpwfoy3YkrTbLBIuEkM'; - const ipa_hcc_register_cmd = 'ipa-hcc register ' + demoToken; +/** + * Represent the page that provide the registration command and check + * when the registration happened from the user. + * @param props provide the uuid and token for the registration process. + * @returns the view for the page updated according the registration + * information. + * @see {@link PageServiceRegistrationProp} about the accepted properties. + * @see {@link WizardPage} about the parent component. + * @public + */ +const PageServiceRegistration = (props: PageServiceRegistrationProp) => { + // FIXME Update the URL with the location for docs + // const installServerPackagesLink = 'https://freeipa.org/page/Quick_Start_Guide'; + const [state, setState] = useState('initial'); + + // FIXME Clean-up when sure it is not needed + // const openInNewWindow = (url: string) => { + // window.open(url, '_blank'); + // }; + + // FIXME Clean-up when sure it is not needed + // const onInstallServerPackagesClick = () => { + // openInNewWindow(installServerPackagesLink); + // }; + + const ipa_hcc_register_cmd = 'ipa-hcc register ' + props.token; const alertTitle = 'Register your directory and domain service'; + // FIXME Update the URL with the location for docs - const linkLearnMoreAbout = 'https://access.redhat.com/articles/1586893'; + const linkLearnMoreAbout = 'https://www.google.es/search?q=freeipa+registering+a+domain+service'; + + const onChangeVerifyRegistry = (newState: VerifyState, domain?: Domain) => { + setState(newState); + if (props.onVerify) { + props.onVerify(newState, domain); + } + }; + + console.log('PageServiceRegistration: uuid=' + props.uuid + '; token=' + props.token); return ( - - - Completing this step registers your directory and domain service, and cannot be undone from the wizard.{' '} - {/* FIXME Q What is the better way to fix the top padding between the link and the text? */} -
- -
-
-
{ - console.debug('onSubmit WizardPage' + String(value)); - }} - > - -
    -
  1. - - To register your Red Hat IdM/IPA server with the Red Hat Hybrid Cloud Console, run the following command in your RHEL IdM (IPA) - server's terminal. - - - {ipa_hcc_register_cmd} - -
  2. -
  3. - Once the process have been completed, run a verification test. -
  4. -
-
+ <> + Register your directory and domain service + e.preventDefault()}> + + Completing this step registers your directory and domain service, and cannot be undone from the wizard.{' '} +
+ +
+
+
    +
  1. + + To register your Red Hat IdM/IPA server with the Red Hat Hybrid Cloud Console, run the following command in your RHEL IdM (IPA) + server's terminal. + + + {ipa_hcc_register_cmd} + +
  2. +
  3. + Once the processes have been completed, run a verification test. +
  4. +
- {/* TODO Add here the new VerifyRegistration component */} -
+ + + + + + ); }; diff --git a/src/Routes/WizardPage/Components/DomainOverview/DomainOverview.scss b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.scss similarity index 100% rename from src/Routes/WizardPage/Components/DomainOverview/DomainOverview.scss rename to src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.scss diff --git a/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.test.tsx b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.test.tsx new file mode 100644 index 0000000..ad7d07b --- /dev/null +++ b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.test.tsx @@ -0,0 +1,10 @@ +// import React from 'react'; +// import { render } from '@testing-library/react'; +// import Page1 from './Page1'; +import '@testing-library/jest-dom'; + +test('expect VerifyRegistry to render children', () => { + // render(); + // expect(screen.getByRole('heading')).toHaveTextContent('Hello'); + return; +}); diff --git a/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.tsx b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.tsx new file mode 100644 index 0000000..90305a1 --- /dev/null +++ b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.tsx @@ -0,0 +1,263 @@ +import React, { useEffect, useState } from 'react'; + +import { AxiosError } from 'axios'; +import { Button, Icon, Stack, StackItem, TextContent } from '@patternfly/react-core'; +import { CheckCircleIcon } from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import { PendingIcon } from '@patternfly/react-icons/dist/esm/icons/pending-icon'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; + +import './VerifyRegistry.scss'; +import { Domain, ResourcesApiFactory } from '../../../../Api'; + +/* Common definitions */ + +export type VerifyState = 'initial' | 'waiting' | 'timed-out' | 'not-found' | 'completed'; + +/* VerifyRegistryIcon component */ + +interface VerifyRegistryIconProps { + state: VerifyState; +} + +const VerifyRegistryIcon = (props: VerifyRegistryIconProps) => { + return ( + <> + {props.state == 'initial' && ( + + + + )} + {props.state == 'waiting' && ( + + + + )} + {props.state == 'timed-out' && ( + + + + )} + {props.state == 'not-found' && ( + + + + )} + {props.state == 'completed' && ( + + + + )} + + ); +}; + +/* VerifyRegistryLabel component */ + +interface VerifyRegistryLabelProps { + state: VerifyState; +} + +const VerifyRegistryLabel = (props: VerifyRegistryLabelProps) => { + return ( + <> + {props.state == 'initial' && Verify registration} + {props.state == 'waiting' && Verify registration} + {props.state == 'timed-out' && Verify registration} + {props.state == 'not-found' && Verify registration} + {props.state == 'completed' && Verify registration} + + ); +}; + +/* VerifyRegistryLabel component */ + +interface VerifyRegistryDescriptionProps { + state: VerifyState; +} + +const VerifyRegistryDescription = (props: VerifyRegistryDescriptionProps) => { + return ( + <> + {props.state == 'initial' && Running verification test} + {props.state == 'waiting' && Waiting for registration data} + {props.state == 'timed-out' && Test timed out} + {props.state == 'not-found' && Registration data not found} + {props.state == 'completed' && Test completed} + + ); +}; + +/* VerifyRegistryyFooter */ + +interface VerifyRegistryFooterProps { + state: VerifyState; + onTest?: () => void; +} + +const VerifyRegistryFooter = (props: VerifyRegistryFooterProps) => { + const linkTroubleshootRegistration = 'https://www.google.com/search?q=freeipa+troubleshooting'; + return ( + <> + {props.state == 'initial' && ( + <> + + + )} + {props.state == 'waiting' && <>} + {props.state == 'timed-out' && ( + <> + + + )} + {props.state == 'not-found' && ( + <> + + + )} + {props.state == 'completed' && ( + <> + + + )} + + ); +}; + +/* VerifyRegistry component */ + +interface VerifyRegistryProps { + state: VerifyState; + uuid: string; + onChange: (newState: VerifyState, domain?: Domain) => void; +} + +const VerifyRegistry = (props: VerifyRegistryProps) => { + const [isPolling, setIsPolling] = useState(true); + + const base_url = '/api/idmsvc/v1'; + const resources_api = ResourcesApiFactory(undefined, base_url, undefined); + const timeout = 3 * 1000; // Seconds + + /** TODO Extract this effect in a hook to simplify this code */ + useEffect(() => { + let intervalId: NodeJS.Timeout | null = null; + let elapsedTime = 0; + let newState: VerifyState = props.state; + let domain: Domain | undefined = undefined; + const stopPolling = (state: VerifyState, domain?: Domain) => { + if (intervalId) { + clearInterval(intervalId); + } + intervalId = null; + setIsPolling(false); + if (state === 'completed') { + props.onChange(state, domain); + } else { + props.onChange(state); + } + }; + if (!isPolling) { + return; + } + const fetchData = async () => { + try { + const response = await resources_api.readDomain(props.uuid, undefined, undefined); + newState = 'completed'; + domain = response.data; + newState = 'completed'; + } catch (error) { + const axiosError = error as AxiosError; + switch (axiosError.code) { + case AxiosError.ECONNABORTED: + case AxiosError.ERR_BAD_OPTION: + case AxiosError.ERR_BAD_OPTION_VALUE: + case AxiosError.ERR_CANCELED: + newState = 'waiting'; + break; + case AxiosError.ERR_DEPRECATED: + case AxiosError.ERR_FR_TOO_MANY_REDIRECTS: + case AxiosError.ERR_NETWORK: + case AxiosError.ETIMEDOUT: + newState = 'timed-out'; + break; + case AxiosError.ERR_BAD_REQUEST: + case AxiosError.ERR_BAD_RESPONSE: + default: + newState = 'waiting'; + break; + } + } + + if (newState !== undefined && newState !== props.state) { + switch (newState) { + case 'timed-out': + case 'waiting': + props.onChange(newState); + // setState(newState); + break; + default: + if (elapsedTime >= timeout) { + newState = 'timed-out'; + stopPolling(newState); + } + break; + case 'completed': + stopPolling(newState, domain); + break; + } + } + elapsedTime += 1000; // Increase elapsed time by 1 second + if (elapsedTime > timeout) { + newState = 'timed-out'; + stopPolling(newState); + } + }; + + fetchData(); + intervalId = setInterval(fetchData, 1000); + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [isPolling]); + + const onRetry = () => { + props.onChange('initial'); + // setState('initial'); + setIsPolling(true); + }; + + return ( + + + + + + + + + + + + + + + + + ); +}; + +// VerifyRegistry.defaultProps = defaultVerifyRegistryProps; + +export default VerifyRegistry; diff --git a/src/Routes/WizardPage/WizardPage.tsx b/src/Routes/WizardPage/WizardPage.tsx index a8edb05..2eb48f7 100644 --- a/src/Routes/WizardPage/WizardPage.tsx +++ b/src/Routes/WizardPage/WizardPage.tsx @@ -1,13 +1,21 @@ -import React, { useEffect, useState } from 'react'; +/** + * This library implement the WizardPage. + * + * The goal is provide the steps to register and add + * a new domain service. + */ +import React, { useContext, useState } from 'react'; import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; -import { Button, Page, PageSection, PageSectionTypes, PageSectionVariants, Wizard } from '@patternfly/react-core'; +import { Button, Page, PageSection, PageSectionTypes, PageSectionVariants, Wizard, WizardStep } from '@patternfly/react-core'; import { PageHeader, PageHeaderTitle } from '@redhat-cloud-services/frontend-components/PageHeader'; import './WizardPage.scss'; import { useNavigate } from 'react-router-dom'; -import { Domain } from '../../Api/api'; +import { Domain, ResourcesApiFactory } from '../../Api/api'; +import { AppContext } from '../../AppContext'; +import { VerifyState } from './Components/VerifyRegistry/VerifyRegistry'; // Lazy load for the wizard pages const PagePreparation = React.lazy(() => import('./Components/PagePreparation/PagePreparation')); @@ -15,75 +23,188 @@ const PageServiceRegistration = React.lazy(() => import('./Components/PageServic const PageServiceDetails = React.lazy(() => import('./Components/PageServiceDetails/PageServiceDetails')); const PageReview = React.lazy(() => import('./Components/PageReview/PageReview')); -const initialDomain: Domain = { - domain_id: '14f3a7a4-32c5-11ee-b40f-482ae3863d30', - domain_name: 'mydomain.example', - auto_enrollment_enabled: true, - title: 'My Domain', - description: 'My Domain Description', - domain_type: 'rhel-idm', - 'rhel-idm': { - realm_name: '', - realm_domains: [], - ca_certs: [], - servers: [], - locations: [], - }, -}; - /** * Wizard page to register a new domain into the service. + * @see {@link PagePreparation} about the preparation page. + * @see {@link PageServiceRegistration} about the registration page. + * @see {@link PageServiceDetails} about the details page. + * @see {@link PageReview} about the review page. */ -const WizardPage: React.FC = () => { +const WizardPage = () => { + const base_url = '/api/idmsvc/v1'; + const resources_api = ResourcesApiFactory(undefined, base_url, undefined); + const appContext = useContext(AppContext); + const domain = appContext.wizard.getDomain(); const navigate = useNavigate(); - // TODO Update the initial state into the context so that - // state can be read/write from the different pages - // into the wizard process. - const [data] = useState(initialDomain); - - useEffect(() => { - insights?.chrome?.appAction?.('default-page'); - }, []); - // FIXME Update the URL with the location for docs const linkLearnMoreAbout = 'https://access.redhat.com/articles/1586893'; - // Event when Close button is clicked + /** Event triggered when Close button is clicked. */ const onCloseClick = () => { - // TODO Not implemented - // What happens on Cancel? (see documentation) + // FIXME A few things pending: + // - Mocal confirmation + // - Confirm => + // - if not registered, dismiss wizard + // - else => DELETE /domains/:domain_id + // - Cancel or close model => Do not dismiss wizard navigate('/domains'); }; + /** Event triggered when Back button is clicked. */ + const onPreviousPage = ( + _newStep: { id?: string | number; name: React.ReactNode }, + _prevStep: { prevId?: string | number; prevName: React.ReactNode } + ) => { + console.log('onPreviousPage fired'); + return; + }; + + /** Event triggered when a specific page is clicked. */ + const onGoToStep = ( + _newStep: { id?: string | number; name: React.ReactNode }, + _prevStep: { prevId?: string | number; prevName: React.ReactNode } + ) => { + console.log('onGoToStep fired'); + return; + }; + + /** Event triggered when the Next button is clicked. */ + const onNextPage = async ({ id }: WizardStep) => { + // FIXME Delete log + console.log('onNextPage fired for id=' + id); + if (id === undefined) { + return; + } + if (typeof id === 'string') { + const [, orderIndex] = id.split('-'); + id = parseInt(orderIndex); + } + if (id === 2) { + // FIXME Clean-up when the token is created into the page 1 + // try { + // const response = await resources_api.createDomainToken({ domain_type: 'rhel-idm' }, undefined, undefined); + // const newData = response.data; + // appContext.wizard.setToken(newData.domain_token); + // appContext.wizard.setUUID(newData.domain_id); + // } catch (error) { + // // TODO Add error hanlder + // console.log('error noNextPage: ' + error); + // appContext.wizard.setToken(''); + // appContext.wizard.setUUID(''); + // } + } + if (id === 4) { + try { + if (domain.domain_id) { + const response = await resources_api.updateDomainUser(domain.domain_id, { + title: domain.title, + description: domain.description, + auto_enrollment_enabled: domain.auto_enrollment_enabled, + }); + if (response.status >= 400) { + // TODO show-up notification with error message + } + } + } catch (error) { + // TODO show-up notification with error message + console.log('error noNextPage: ' + error); + } + } + }; + + const initCanJumpPage1 = true; + const initCanJumpPage2 = initCanJumpPage1 && domain.domain_id != '' && appContext.wizard.getToken() != ''; + const initCanJumpPage3 = initCanJumpPage2 && appContext.wizard.getRegisteredStatus() === 'completed'; + const initCanJumpPage4 = initCanJumpPage3 && domain.title !== undefined && domain.title.length > 0; + + const [canJumpPage1] = useState(initCanJumpPage1); + const [canJumpPage2, setCanJumpPage2] = useState(initCanJumpPage2); + const [canJumpPage3, setCanJumpPage3] = useState(initCanJumpPage3); + const [canJumpPage4, setCanJumpPage4] = useState(initCanJumpPage4); + + const onToken = (token: string, domain_id: string, expiration: number) => { + console.log('WizardPage.OnToken fired: token=' + token + '; domain_id=' + domain_id + '; expiration=' + expiration); + if (token != '') { + setCanJumpPage2(true); + } else { + setCanJumpPage2(false); + } + }; + + const onVerify = (value: VerifyState, data?: Domain) => { + appContext.wizard.setRegisteredStatus(value); + if (value === 'completed') { + if (data) { + appContext.wizard.setDomain(data); + } + setCanJumpPage3(true); + } else { + setCanJumpPage3(false); + } + }; + + const onChangeTitle = (value: string) => { + appContext.wizard.setDomain({ ...domain, title: value }); + if (value.length > 0) { + setCanJumpPage4(true); + } else { + setCanJumpPage4(false); + } + }; + + const onChangeDescription = (value: string) => { + appContext.wizard.setDomain({ ...domain, description: value }); + }; + + const onChangeAutoEnrollment = (value: boolean) => { + appContext.wizard.setDomain({ ...domain, auto_enrollment_enabled: value }); + }; + + /** Configure the wizard pages. */ const steps = [ { // This page only display the pre-requisites + id: 1, name: 'Preparation', - component: , + component: , + canJumpTo: canJumpPage1, }, { + id: 2, name: 'Service registration', - // FIXME Pass here the 'registering.domain' field from the context - // FIXME Pass here the 'registering.token' field from the context - component: , + component: , + canJumpTo: canJumpPage2, }, { + id: 3, name: 'Service details', // FIXME Pass here the 'registering.domain' field from the context - component: , + component: ( + + ), + canJumpTo: canJumpPage3, }, { + id: 4, name: 'Review', - // FIXME Pass here the 'registering.domain' field from the context - component: , + component: , + nextButtonText: 'Finish', + canJumpTo: canJumpPage4, }, ]; const title = 'Register directory and domain service'; return ( - + <> @@ -98,15 +219,23 @@ const WizardPage: React.FC = () => { iconPosition="right" href={linkLearnMoreAbout} > - Learn more about the directory and domain services. + Learn more about the directory and domain services{' '}

- +
-
+ ); };