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..d49ee55 100644 --- a/docs/98-tips.md +++ b/docs/98-tips.md @@ -28,22 +28,29 @@ For instance: import './sample-component.scss'; import React from 'react'; -interface SampleComponentProp { +/* SampleComponent component */ + +interface SampleComponentProps { name: string; description: string; } +const defaultSampleComponentProps: SampleComponentProps = { + name: '', + description: '', +}; + /** * This is a dumb component that only recieves properties from a smart component. * Dumb components are usually functions and not classes. * * @param props the props given by the smart component. */ -const SampleComponent: React.FC = (props) => { +const SampleComponent: React.FC = (props) => { return {props.children} ; }; -SampleComponent.displayName = 'SampleComponent'; +SampleComponent.defaultProps = defaultSampleComponentProps; export default SampleComponent; ``` @@ -133,6 +140,43 @@ 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 defaultMyComponentProps: MyComponentProps = { + value: '', + onChange: (value: string) => { return; }, +}; + +const MyComponent: React.FC = (props) => { + const [state, setState] = useState(props.value); + const onChange: (value: string) => { + setState(value); + props.onChange(state); + }; + return ( + + ); +}; + +MyComponent.defaultProps = defaultMyComponentProps; +``` + +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 @@ -156,6 +200,16 @@ Said this, we can quickly identify some specific cases for our repository: - The register domain in process, to show and update the info as we advance in the wizard. +## Document your components + +TODO + +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. + ## References Helpful articles: 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/AppContext.tsx b/src/AppContext.tsx index ed5c41b..fe1457c 100644 --- a/src/AppContext.tsx +++ b/src/AppContext.tsx @@ -1,9 +1,22 @@ import { createContext } from 'react'; import { Domain } from './Api'; +import { VerifyState } from './Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry'; export interface IAppContext { domains: Domain[]; setDomains: (domains: Domain[]) => void; + wizard: { + getToken: () => string; + setToken: (value: string) => void; + getUUID: () => string; + setUUID: (value: string) => void; + title: string; + setTitle: (value: string) => void; + description: string; + setDescription: (value: string) => void; + getRegisteredStatus: () => VerifyState; + setRegisteredStatus: (value: VerifyState) => void; + }; // wizardDomain?: Domain; // setWizardDomain: (domain?: Domain) => void; } @@ -13,6 +26,34 @@ export const AppContext = createContext({ setDomains: (domains: Domain[]) => { throw new Error('Function "setDomains" not implemented: domains=' + domains); }, + wizard: { + getToken: (): string => { + return ''; + }, + setToken: (value: string) => { + throw new Error('Function "setToken" not implemented: value=' + value); + }, + getUUID: (): string => { + return ''; + }, + setUUID: (value: string) => { + throw new Error('Function "setUUID" not implemented: value=' + value); + }, + title: '', + setTitle: (value: string) => { + throw new Error('Function "setTitle" not implemented: value=' + value); + }, + description: '', + setDescription: (value: string) => { + throw new Error('Function "setDescription" not implemented: value=' + value); + }, + getRegisteredStatus: (): VerifyState => { + return 'initial'; + }, + setRegisteredStatus: (value: VerifyState) => { + throw new Error('Function "setRegisteredStatus" not implemented: value=' + value); + }, + }, // wizardDomain: undefined, // setWizardDomain: (domain?: Domain) => { // throw new Error('Function "setWizardDomain" not implemented: domain=' + domain); diff --git a/src/AppEntry.tsx b/src/AppEntry.tsx index 8bf2bf4..15635bb 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,67 @@ 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 [wizardTitle, setWizardTitle] = useState(''); + const [wizardDescription, setWizardDescription] = useState(''); + const [wizardUUID, setWizardUUID] = useState(''); + 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 cbGetWizardUUID = (): string => { + return wizardUUID; + }; + const cbSetWizardUUID = (value: string) => { + setWizardUUID(value); + }; + const cbSetWizardTitle = (title: string) => { + appContext.wizard.title = title; + setWizardTitle(title); + }; + const cbSetWizardDescription = (description: string) => { + appContext.wizard.description = description; + setWizardDescription(description); + }; + 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..06c3515 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,16 @@ const Header = () => { }; const EmptyContent = () => { + const linkLearnMoreAbout = 'https://access.redhat.com/articles/1586893'; const navigate = useNavigate(); + const appContext = useContext(AppContext); const handleOpenWizard = () => { + appContext.wizard.setToken(''); + appContext.wizard.setUUID(''); + + appContext.wizard.setTitle(''); + appContext.wizard.setDescription(''); navigate('/domains/wizard', { replace: true }); }; @@ -62,21 +69,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 host 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.
@@ -205,9 +225,9 @@ const DefaultPage = () => { const [perPage] = useState(10); const [offset, setOffset] = useState(0); - useEffect(() => { - insights?.chrome?.appAction?.('default-page'); - }, []); + // useEffect(() => { + // insights?.chrome?.appAction?.('default-page'); + // }, []); console.log('INFO:DefaultPage render'); 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..ac109df 100644 --- a/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx +++ b/src/Routes/WizardPage/Components/PagePreparation/PagePreparation.tsx @@ -1,26 +1,64 @@ -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'; +interface PagePreparationProps { + token?: string; + onToken?: (token: string, domain_id: string, expiration: number) => void; +} + +const defaultPagePreparationProps: PagePreparationProps = { + token: undefined, + onToken: undefined, +}; + +const PagePreparation: React.FC = (props) => { + // 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_id = appContext.wizard.getUUID(); + + /** + * 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); + appContext.wizard.setUUID(value.data.domain_id); + props.onToken?.(value.data.domain_token, value.data.domain_id, value.data.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 +68,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 +101,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 +110,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 wether 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. +
); }; +PagePreparation.defaultProps = defaultPagePreparationProps; + export default PagePreparation; diff --git a/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx b/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx index 3b9e8cf..74d81be 100644 --- a/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx +++ b/src/Routes/WizardPage/Components/PageServiceDetails/PageServiceDetails.tsx @@ -1,5 +1,5 @@ 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'; @@ -28,7 +28,7 @@ const PageServiceDetails: React.FC<{ data: Domain }> = (props) => { > Service Details - + @@ -44,7 +44,10 @@ const PageServiceDetails: React.FC<{ data: Domain }> = (props) => { - 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..5524f77 100644 --- a/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx +++ b/src/Routes/WizardPage/Components/PageServiceRegistration/PageServiceRegistration.tsx @@ -1,58 +1,77 @@ -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'; interface PageServiceRegistrationProp { - data?: Domain; - token?: string; + uuid: string; + token: string; + 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; + // FIXME Update the URL with the location for docs + const installServerPackagesLink = 'https://freeipa.org/page/Quick_Start_Guide'; + const [state, setState] = useState('initial'); + + const openInNewWindow = (url: string) => { + window.open(url, '_blank'); + }; + + 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) => { + setState(newState); + props.onVerify(newState); + }; + + 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? */} -
- -
-
+ Register your directory and domain service
{ 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. -
-
+ + 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/VerifyRegistry/VerifyRegistry.scss b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.scss new file mode 100644 index 0000000..861f8be --- /dev/null +++ b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.scss @@ -0,0 +1,2 @@ +@import '~@redhat-cloud-services/frontend-components-utilities/styles/variables'; + 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..ee33d27 --- /dev/null +++ b/src/Routes/WizardPage/Components/VerifyRegistry/VerifyRegistry.tsx @@ -0,0 +1,301 @@ +import React, { useEffect, useState } from 'react'; + +import { AxiosError } from 'axios'; +import { Button, Icon, Label, 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 { ResourcesApiFactory } from '../../../../Api'; + +/* Common definitions */ + +export type VerifyState = 'initial' | 'waiting' | 'timed-out' | 'not-found' | 'completed'; + +/* VerifyRegistryIcon component */ + +interface VerifyRegistryIconProps { + state: VerifyState; +} + +// const defaultVerifyRegistryIconProps: VerifyRegistryIconProps = { +// state: 'initial', +// }; + +const VerifyRegistryIcon = (props: VerifyRegistryIconProps) => { + return ( + <> + {props.state == 'initial' && ( + + )} + {props.state == 'waiting' && ( + + )} + {props.state == 'timed-out' && ( + + )} + {props.state == 'not-found' && ( + + )} + {props.state == 'completed' && ( + + )} + + ); +}; + +// VerifyRegistryIcon.defaultProps = defaultVerifyRegistryIconProps; + +/* VerifyRegistryLabel component */ + +interface VerifyRegistryLabelProps { + state: VerifyState; +} + +// const defaultVerifyRegistryLabelProps: VerifyRegistryLabelProps = { +// state: 'initial', +// }; + +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.defaultProps = defaultVerifyRegistryLabelProps; + +/* VerifyRegistryLabel component */ + +interface VerifyRegistryDescriptionProps { + state: VerifyState; +} + +// const defaultVerifyRegistryDescriptionProps: VerifyRegistryDescriptionProps = { +// state: 'initial', +// }; + +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} + + ); +}; + +// VerifyRegistryDescription.defaultProps = defaultVerifyRegistryDescriptionProps; + +/* VerifyRegistryyFooter */ + +interface VerifyRegistryFooterProps { + state: VerifyState; + onTest?: () => void; +} + +// const defaultVerifyRegistryFooterProps: VerifyRegistryFooterProps = { +// state: 'initial', +// onTest: () => { +// console.log('VerifyRegistryFooter.onTest event fired'); +// }, +// }; + +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' && ( + <> + + + )} + + ); +}; + +// VerifyRegistryFooter.defaultProps = defaultVerifyRegistryFooterProps; + +/* VerifyRegistry component */ + +interface VerifyRegistryProps { + state: VerifyState; + uuid: string; + onChange: (newState: VerifyState) => void; +} + +// const defaultVerifyRegistryProps: VerifyRegistryProps = { +// state: 'initial', +// uuid: '', +// onChange: (value: VerifyState) => { +// console.log('VerifyRegistry:onChange fired: value=' + value); +// }, +// }; + +const VerifyRegistry = (props: VerifyRegistryProps) => { + // const [state, setState] = useState(props.state); + const [isPolling, setIsPolling] = useState(true); + + const base_url = '/api/idmsvc/v1'; + const resources_api = ResourcesApiFactory(undefined, base_url, undefined); + const timeout = 30 * 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; + const stopPolling = (state: VerifyState) => { + if (intervalId) { + clearInterval(intervalId); + } + intervalId = null; + setIsPolling(false); + // setState(state); + props.onChange(state); + }; + if (!isPolling) { + return; + } + const fetchData = async () => { + try { + // setState('waiting'); + const response = await resources_api.readDomain(props.uuid, undefined, undefined); + const newData = 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); + break; + } + } + elapsedTime += 1000; // Increase elapsed time by 1 second + }; + + 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..26ca1a4 100644 --- a/src/Routes/WizardPage/WizardPage.tsx +++ b/src/Routes/WizardPage/WizardPage.tsx @@ -1,13 +1,15 @@ -import React, { useEffect, useState } from 'react'; +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')); @@ -35,48 +37,149 @@ const initialDomain: Domain = { * Wizard page to register a new domain into the service. */ const WizardPage: React.FC = () => { + const base_url = '/api/idmsvc/v1'; + const resources_api = ResourcesApiFactory(undefined, base_url, undefined); + const appContext = useContext(AppContext); 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); + const [data, setData] = useState(initialDomain); + const [verifyState, setVerifyState] = useState('initial'); - useEffect(() => { - insights?.chrome?.appAction?.('default-page'); - }, []); + // FIXME Clean-up if this is not really necessary + // 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) 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) { + 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 { + // appContext.wizard.setTitle(); + // appContext.wizard.setDescription(newData.domain_id); + const response = await resources_api.updateDomainUser(appContext.wizard.getUUID(), { + title: appContext.wizard.title, + description: appContext.wizard.description, + // FIXME Read value from the context + auto_enrollment_enabled: false, + }); + const newData = response.data; + } catch (error) { + // TODO Add error hanlder + console.log('error noNextPage: ' + error); + } + } + } + }; + + const [canJumpPage1, setCanJumpPage1] = useState(true); + const [canJumpPage2, setCanJumpPage2] = useState(appContext.wizard.getUUID() != '' && appContext.wizard.getToken() != ''); + const [canJumpPage3, setCanJumpPage3] = useState(appContext.wizard.getRegisteredStatus() === 'completed'); + const [canJumpPage4, setCanJumpPage4] = useState(false); + + const onToken = (token: string, domain_id: string, expiration: number) => { + console.log('WizardPage.OnToken fired'); + if (token != '') { + setCanJumpPage2(true); + } else { + setCanJumpPage2(false); + } + }; + + const onVerify = (value: VerifyState, data?: Domain) => { + setVerifyState(value); + if (value === 'completed') { + if (data) { + setData(data); + } + setCanJumpPage3(true); + setCanJumpPage4(true); + } else { + setCanJumpPage3(false); + setCanJumpPage4(false); + } + }; + + /** 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: , + canJumpTo: canJumpPage3, }, { + id: 4, name: 'Review', // FIXME Pass here the 'registering.domain' field from the context component: , + nextButtonText: 'Finish', + canJumpTo: canJumpPage4, }, ]; @@ -103,7 +206,15 @@ const WizardPage: React.FC = () => {

- +