diff --git a/apps/console/src/features/core/constants/app-constants.ts b/apps/console/src/features/core/constants/app-constants.ts index dbe8c772a95..06d42eac2c3 100644 --- a/apps/console/src/features/core/constants/app-constants.ts +++ b/apps/console/src/features/core/constants/app-constants.ts @@ -275,6 +275,9 @@ export class AppConstants { [ "CONNECTION_EDIT", `${ AppConstants.getDeveloperViewBasePath() }/connections/:id` ], [ "CUSTOMIZE", `${ AppConstants.getMainViewBasePath() }/customize` ], [ "DEVELOPER_OVERVIEW", `${ AppConstants.getDeveloperViewBasePath() }/overview` ], + [ "EMAIL_DOMAIN_ASSIGN", `${AppConstants.getAdminViewBasePath()}/email-domain-assign` ], + [ "EMAIL_DOMAIN_DISCOVERY", `${AppConstants.getAdminViewBasePath()}/email-domain-discovery` ], + [ "EMAIL_DOMAIN_UPDATE", `${AppConstants.getAdminViewBasePath()}/email-domain-edit/:id` ], [ "EMAIL_PROVIDER", `${ AppConstants.getDeveloperViewBasePath() }/email-provider` ], [ "EMAIL_TEMPLATE_TYPES", `${ AppConstants.getAdminViewBasePath() }/email-templates` ], [ "EMAIL_TEMPLATES", `${ AppConstants.getAdminViewBasePath() }/email-templates/:templateTypeId` ], diff --git a/apps/console/src/features/core/models/config.ts b/apps/console/src/features/core/models/config.ts index bb65ab70a67..f95b4a380f6 100644 --- a/apps/console/src/features/core/models/config.ts +++ b/apps/console/src/features/core/models/config.ts @@ -132,6 +132,10 @@ export interface FeatureConfigInterface { * Organization management feature. */ organizations?: FeatureAccessConfigInterface; + /** + * Organization discovery feature. + */ + organizationDiscovery?: FeatureAccessConfigInterface; /** * Organization role management feature. */ diff --git a/apps/console/src/features/core/utils/route-utils.ts b/apps/console/src/features/core/utils/route-utils.ts index e487dde9623..31e90b6c27e 100644 --- a/apps/console/src/features/core/utils/route-utils.ts +++ b/apps/console/src/features/core/utils/route-utils.ts @@ -267,6 +267,12 @@ export class RouteUtils { name: "User Attributes & Stores" }; + const organizationManagement: Omit = { + icon: BuildingGearIcon, + id: "organizationManagement", + name: "Organization Management" + }; + const branding: Omit = { icon: PaletteIcon, id: "customization", @@ -399,7 +405,13 @@ export class RouteUtils { }, { category: manage, - id: "organizations" + id: "organizations", + parent: organizationManagement + }, + { + category: manage, + id: "organizationDiscovery", + parent: organizationManagement }, { category: monitoring, diff --git a/apps/console/src/features/organization-discovery/api/index.ts b/apps/console/src/features/organization-discovery/api/index.ts new file mode 100644 index 00000000000..e69bfcf1b17 --- /dev/null +++ b/apps/console/src/features/organization-discovery/api/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./organization-discovery"; diff --git a/apps/console/src/features/organization-discovery/api/organization-discovery.ts b/apps/console/src/features/organization-discovery/api/organization-discovery.ts new file mode 100644 index 00000000000..11fe8f6b1ad --- /dev/null +++ b/apps/console/src/features/organization-discovery/api/organization-discovery.ts @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + AsgardeoSPAClient, + HttpClientInstance, + HttpError, + HttpRequestConfig, + HttpResponse +} from "@asgardeo/auth-react"; +import { IdentityAppsApiException } from "@wso2is/core/exceptions"; +import { HttpMethods } from "@wso2is/core/models"; +import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; +import { store } from "../../core"; +import { + OrganizationDiscoveryAttributeDataInterface, + OrganizationDiscoveryConfigInterface, + OrganizationListInterface, + OrganizationListWithDiscoveryInterface, + OrganizationResponseInterface +} from "../models"; + +/** + * Get an axios instance. + * + */ +const httpClient: HttpClientInstance = AsgardeoSPAClient.getInstance() + .httpRequest.bind(AsgardeoSPAClient.getInstance()) + .bind(AsgardeoSPAClient.getInstance()); + +/** + * Get organization discovery configurations. + * + * @returns a promise containing the response + */ +export const getOrganizationDiscoveryConfig = ( +): Promise => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "GET", + url: `${ store.getState().config.endpoints.organizations }/organization-configs/discovery` + }; + + return httpClient(config) + .then((response: HttpResponse) => { + return Promise.resolve(response?.data); + }) + .catch((error: HttpError) => { + return Promise.reject(error); + }); +}; + +/** + * Add organization discovery configurations + * + * @param properties - Data that needs to be updated. + */ +export const addOrganizationDiscoveryConfig = ( + properties: OrganizationDiscoveryConfigInterface +): Promise => { + const requestConfig: AxiosRequestConfig = { + data: properties, + headers: { + "Content-Type": "application/json" + }, + method: HttpMethods.POST, + url: `${ store.getState().config.endpoints.organizations }/organization-configs/discovery` + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 201) { + return Promise.reject(new Error("Failed to add organization discovery configs.")); + } + + return Promise.resolve(response?.data); + }).catch((error: AxiosError) => { + return Promise.reject(error?.response?.data); + }); +}; + +/** + * Delete organization discovery configurations. + * + * @returns a promise containing the response + */ +export const deleteOrganizationDiscoveryConfig = ( +): Promise => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "DELETE", + url: `${ store.getState().config.endpoints.organizations }/organization-configs/discovery` + }; + + return httpClient(config) + .then((response: HttpResponse) => { + if (response?.status !== 204) { + return Promise.reject(new Error("Failed to delete organization discovery configs.")); + } + + return Promise.resolve(response?.data); + }) + .catch((error: HttpError) => { + return Promise.reject(error); + }); +}; + +/** + * Get a list of organizations. + * + * @param filter - The filter query. + * @param _offset - The maximum number of organizations to return. + * @param _limit - Number of records to skip for pagination. + * + * @returns a promise containing the response + */ +export const getOrganizationDiscovery = ( + filter?: string, + _offset?: number, + _limit?: number +): Promise< OrganizationListWithDiscoveryInterface> => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "GET", + params: { + filter + }, + url: `${ store.getState().config.endpoints.organizations }/organizations/discovery` + }; + + return httpClient(config) + .then((response: HttpResponse< OrganizationListWithDiscoveryInterface>) => { + if (response.status !== 200) { + return Promise.reject(new Error("Failed to get organizations with email domain attributes.")); + } + + return Promise.resolve(response?.data); + }) + .catch((error: HttpError) => { + return Promise.reject(error?.response?.data); + }); +}; + +/** + * Get the organization discovery data with the given id. + * + * @param id - The organization id. + * + * @returns a promise containing the response + */ +export const getOrganizationDiscoveryAttributes = (id: string): +Promise => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "GET", + url: `${ store.getState().config.endpoints.organizations }/organizations/${ id }/discovery` + }; + + return httpClient(config) + .then((response: HttpResponse) => { + if (response.status !== 200) { + return Promise.reject(new Error("Failed to get the organization.")); + } + + return Promise.resolve(response?.data); + }) + .catch((error: IdentityAppsApiException) => { + return Promise.reject(error?.response?.data); + }); +}; + +/** + * Update discovery attributes of an organization. + * + * @param properties - Data that needs to be updated. + */ +export const updateOrganizationDiscoveryAttributes = ( + id: string, + properties: OrganizationDiscoveryAttributeDataInterface +): Promise => { + const requestConfig: AxiosRequestConfig = { + data: properties, + headers: { + "Content-Type": "application/json" + }, + method: HttpMethods.PUT, + url: `${ store.getState().config.endpoints.organizations }/organizations/${ id }/discovery` + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 200) { + return Promise.reject(new Error("Failed to update organization discovery attributes.")); + } + + return Promise.resolve(response?.data); + }).catch((error: AxiosError) => { + return Promise.reject(error?.response?.data); + }); +}; + +/** + * Get a list of organizations. + * + * @param filter - The filter query. + * @param limit - The maximum number of organizations to return. + * @param after - The previous range of data to be returned. + * @param before - The next range of data to be returned. + * @param recursive - Whether we need to do a recursive search + * @param isRoot - Whether the organization is the root + * + * @returns a promise containing the response + */ +export const getOrganizations = ( + filter: string, + limit: number, + after: string, + before: string, + recursive: boolean, + isRoot: boolean = false +): Promise => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "GET", + params: { + after, + before, + filter, + limit, + recursive + }, + url: `${ isRoot + ? store.getState().config.endpoints.rootOrganization + : store.getState().config.endpoints.organizations }/organizations` + }; + + return httpClient(config) + .then((response: HttpResponse) => { + if (response.status !== 200) { + return Promise.reject(new Error("Failed to get organizations.")); + } + + return Promise.resolve(response?.data); + }) + .catch((error: HttpError) => { + return Promise.reject(error?.response?.data); + }); +}; + +/** + * Get the organization with the given id. + * + * @param id - The organization id. + * @param showChildren - Specifies if the child organizations should be returned. + * + * @returns a promise containing the response + */ +export const getOrganization = (id: string, showChildren?: boolean): Promise => { + const config: HttpRequestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: "GET", + params: { + showChildren + }, + url: `${ store.getState().config.endpoints.organizations }/organizations/${ id }` + }; + + return httpClient(config) + .then((response: HttpResponse) => { + if (response.status !== 200) { + return Promise.reject(new Error("Failed to get the organization.")); + } + + return Promise.resolve(response?.data); + }) + .catch((error: IdentityAppsApiException) => { + return Promise.reject(error?.response?.data); + }); +}; diff --git a/apps/console/src/features/organization-discovery/components/edit-organization/edit-organization-email-domains.tsx b/apps/console/src/features/organization-discovery/components/edit-organization/edit-organization-email-domains.tsx new file mode 100644 index 00000000000..fd2dc32d75e --- /dev/null +++ b/apps/console/src/features/organization-discovery/components/edit-organization/edit-organization-email-domains.tsx @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Autocomplete, + AutocompleteRenderGetTagProps, + AutocompleteRenderInputParams +} from "@mui/material"; +import { Chip, TextField } from "@oxygen-ui/react"; +import InputLabel from "@oxygen-ui/react/InputLabel/InputLabel"; +import { + AlertLevels, + SBACInterface, + TestableComponentInterface +} from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { + ContentLoader, + EmphasizedSegment, + PrimaryButton +} from "@wso2is/react-components"; +import { IdentityAppsError } from "modules/core/dist/types/errors"; +import React, { + FunctionComponent, + ReactElement, + useState +} from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import { Divider, Grid, Form as SemanticForm } from "semantic-ui-react"; +import { FeatureConfigInterface } from "../../../core"; +import { updateOrganizationDiscoveryAttributes } from "../../api"; +import { + OrganizationDiscoveryAttributeDataInterface, + OrganizationResponseInterface +} from "../../models"; + +interface EditOrganizationEmailDomainsPropsInterface + extends SBACInterface, + TestableComponentInterface { + /** + * Organization info + */ + organization: OrganizationResponseInterface; + /** + * Organization discovery info + */ + organizationDiscoveryData: OrganizationDiscoveryAttributeDataInterface; + /** + * Is read only view + */ + isReadOnly: boolean; + /** + * Callback for when organization update + */ + onOrganizationUpdate: (organizationId: string) => void; +} + +/** + * Organization overview component. + * + * @param props - Props injected to the component. + * @returns Functional component. + */ +export const EditOrganizationEmailDomains: FunctionComponent = ( + props: EditOrganizationEmailDomainsPropsInterface +): ReactElement => { + const { + organization, + organizationDiscoveryData, + isReadOnly, + onOrganizationUpdate, + [ "data-testid" ]: testId + } = props; + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ isSubmitting, setIsSubmitting ] = useState(false); + const [ emailDomainData, setEmailDomainData ] = useState(); + + const optionsArray: string[] = []; + + const handleSubmit = () => { + setIsSubmitting(true); + + const emailDomainDiscoveryData: OrganizationDiscoveryAttributeDataInterface = { + attributes: [ + { + type: "emailDomain", + values: emailDomainData + } + ] + }; + + updateOrganizationDiscoveryAttributes(organization.id, emailDomainDiscoveryData) + .then(() => { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "updateOrganizationDiscoveryAttributes.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "updateOrganizationDiscoveryAttributes.success.message" + ) + }) + ); + + onOrganizationUpdate(organization.id); + }) + .catch((error: IdentityAppsError) => { + if (error.description) { + dispatch( + addAlert({ + description: error.description, + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "updateOrganizationDiscoveryAttributes.error.message" + ) + }) + ); + + return; + } + + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications" + + ".updateOrganizationDiscoveryAttributes.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications" + + ".updateOrganizationDiscoveryAttributes.genericError.message" + ) + }) + ); + }) + .finally(() => setIsSubmitting(false)); + }; + + return organization ? ( + <> + + + + + + + + + + + + + + + + ) : ( + + ); +}; + +/** + * Default props for the component. + */ +EditOrganizationEmailDomains.defaultProps = { + "data-testid": "edit-organization-email-domains" +}; diff --git a/apps/console/src/features/organization-discovery/components/index.ts b/apps/console/src/features/organization-discovery/components/index.ts new file mode 100644 index 00000000000..68d9011a3f7 --- /dev/null +++ b/apps/console/src/features/organization-discovery/components/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./organization-list-with-discovery"; diff --git a/apps/console/src/features/organization-discovery/components/organization-list-with-discovery.tsx b/apps/console/src/features/organization-discovery/components/organization-list-with-discovery.tsx new file mode 100644 index 00000000000..89c401656b8 --- /dev/null +++ b/apps/console/src/features/organization-discovery/components/organization-list-with-discovery.tsx @@ -0,0 +1,323 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AccessControlConstants, Show } from "@wso2is/access-control"; +import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers"; +import { + IdentifiableComponentInterface, + LoadableComponentInterface +} from "@wso2is/core/models"; +import { + DataTable, + EmptyPlaceholder, + GenericIcon, + LinkButton, + PrimaryButton, + TableActionsInterface, + TableColumnInterface +} from "@wso2is/react-components"; +import isEmpty from "lodash-es/isEmpty"; +import React, { FunctionComponent, ReactElement, ReactNode, SyntheticEvent } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { Header, Icon, SemanticICONS } from "semantic-ui-react"; +import { + AppConstants, + AppState, + EventPublisher, + FeatureConfigInterface, + UIConstants, + history +} from "../../core"; +import { getEmptyPlaceholderIllustrations } from "../../core/configs/ui"; +import { OrganizationIcon } from "../configs"; +import { OrganizationManagementConstants } from "../constants"; +import { OrganizationDiscoveryInterface, OrganizationListWithDiscoveryInterface } from "../models"; + +/** + * + * Proptypes for the organizations list component. + */ +export interface OrganizationListWithDiscoveryPropsInterface + extends LoadableComponentInterface, + IdentifiableComponentInterface { + /** + * Default list item limit. + */ + defaultListItemLimit?: number; + /** + * Organization list. + */ + list: OrganizationListWithDiscoveryInterface; + /** + * Callback for the search query clear action. + */ + onSearchQueryClear?: () => void; + /** + * Callback to be fired when clicked on the empty list placeholder action. + */ + onEmptyListPlaceholderActionClick?: () => void; + /** + * Search query for the list. + */ + searchQuery?: string; + /** + * Enable selection styles. + */ + selection?: boolean; + /** + * Show list item actions. + */ + showListItemActions?: boolean; + /** + * Is the list rendered on a portal. + */ + isRenderedOnPortal?: boolean; +} + +/** + * Organization list component. + * + * @param props - Props injected to the component. + * + * @returns + */ +export const OrganizationListWithDiscovery: FunctionComponent = ( + props: OrganizationListWithDiscoveryPropsInterface +): ReactElement => { + const { + defaultListItemLimit, + isLoading, + list, + onEmptyListPlaceholderActionClick, + onSearchQueryClear, + searchQuery, + selection, + showListItemActions, + isRenderedOnPortal, + [ "data-componentid" ]: componentId + } = props; + + const { t } = useTranslation(); + + const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); + const featureConfig: FeatureConfigInterface = useSelector((state: AppState) => state.config.ui.features); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + /** + * Redirects to the organizations edit page when the edit button is clicked. + * + * @param organizationId - Organization id. + */ + const handleOrganizationEmailDomainEdit = (organizationId: string): void => { + history.push({ + pathname: AppConstants.getPaths() + .get("EMAIL_DOMAIN_UPDATE") + .replace(":id", organizationId) + }); + }; + + /** + * Resolves data table columns. + * + * @returns + */ + const resolveTableColumns = (): TableColumnInterface[] => { + return [ + { + allowToggleVisibility: false, + dataIndex: "name", + id: "name", + key: "name", + render: (organization: OrganizationDiscoveryInterface): ReactNode => { + return ( +
+ + + { organization.organizationName } + +
+ ); + }, + title: t("console:manage.features.organizations.list.columns.name") + }, + { + allowToggleVisibility: false, + dataIndex: "action", + id: "actions", + key: "actions", + textAlign: "right", + title: t("console:manage.features.organizations.list.columns.actions") + } + ]; + }; + + /** + * Resolves data table actions. + * + * @returns + */ + const resolveTableActions = (): TableActionsInterface[] => { + if (!showListItemActions) { + return; + } + + return [ + { + "data-componentid": `${ componentId }-item-edit-button`, + hidden: (): boolean => + !isFeatureEnabled( + featureConfig?.organizationDiscovery, + OrganizationManagementConstants.FEATURE_DICTIONARY.get("ORGANIZATION_EMAIL_DOMAIN_UPDATE") + ), + icon: (): SemanticICONS => { + + return !hasRequiredScopes( + featureConfig?.organizationDiscovery, + featureConfig?.organizationDiscovery?.scopes?.update, + allowedScopes + ) + ? "eye" + : "pencil alternate"; + }, + onClick: (e: SyntheticEvent, organization: OrganizationDiscoveryInterface): void => + handleOrganizationEmailDomainEdit(organization.organizationId), + popupText: (): string => { + + return !hasRequiredScopes( + featureConfig?.organizationDiscovery, + featureConfig?.organizationDiscovery?.scopes?.update, + allowedScopes + ) + ? t("common:view") + : t("common:edit"); + }, + renderer: "semantic-icon" + } + ]; + }; + + /** + * Resolve the relevant placeholder. + * + * @returns + */ + const showPlaceholders = (): ReactElement => { + if (searchQuery && (isEmpty(list) || list?.organizations?.length === 0)) { + return ( + + { t("console:manage.placeholders.emptySearchResult.action") } + ) + } + image={ getEmptyPlaceholderIllustrations().emptySearch } + imageSize="tiny" + title={ t("console:manage.placeholders.emptySearchResult.title") } + subtitle={ [ + t("console:manage.placeholders.emptySearchResult.subtitles.0", { + // searchQuery looks like "name co OrganizationName", so we only remove the filter string + // only to get the actual user entered query + query: searchQuery.split("organizationName co ")[1] + }), + t("console:manage.placeholders.emptySearchResult.subtitles.1") + ] } + data-componentid={ `${ componentId }-empty-search-placeholder` } + /> + ); + } + + // When the search returns empty. + if (isEmpty(list) || list?.organizations?.length === 0) { + return ( + + { + eventPublisher.publish(componentId + "-click-assign-email-domain-button"); + onEmptyListPlaceholderActionClick(); + } } + > + + { t("console:manage.features.organizationDiscovery.placeholders.emptyList.action") } + + + ) + } + image={ getEmptyPlaceholderIllustrations().newList } + imageSize="tiny" + subtitle={ t("console:manage.features.organizationDiscoverys.placeholders.emptyList.subtitles") } + data-componentid={ `${ componentId }-empty-placeholder` } + /> + ); + } + + return null; + }; + + return ( + <> + + className="organizations-table" + isLoading={ isLoading } + loadingStateOptions={ { + count: defaultListItemLimit ?? UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT, + imageType: "square" + } } + actions={ resolveTableActions() } + columns={ resolveTableColumns() } + data={ list?.organizations } + onRowClick={ (e: SyntheticEvent, organization: OrganizationDiscoveryInterface): void => { + handleOrganizationEmailDomainEdit(organization.organizationId); + } + } + placeholders={ showPlaceholders() } + selectable={ selection } + showHeader={ false } + transparent={ !isLoading && showPlaceholders() !== null } + data-componentid={ componentId } + /> + + ); +}; + +/** + * Default props for the component. + */ +OrganizationListWithDiscovery.defaultProps = { + "data-componentid": "organization-list-with-discovery", + selection: true, + showListItemActions: true +}; diff --git a/apps/console/src/features/organization-discovery/configs/endpoints.ts b/apps/console/src/features/organization-discovery/configs/endpoints.ts new file mode 100644 index 00000000000..832f3905e19 --- /dev/null +++ b/apps/console/src/features/organization-discovery/configs/endpoints.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OrganizationResourceEndpointsInterface } from "../models"; + +/** + * Get the resource endpoints for the Application Management feature. + * + * @param serverHost - Server Host. + * + * @returns OrganizationResourceEndpointsInterface + */ +export const getOrganizationsResourceEndpoints = ( + serverHostWithOrgPath: string, + serverHost: string +): OrganizationResourceEndpointsInterface => { + return { + breadcrumb: `${serverHostWithOrgPath}/api/users/v1/me/organizations/root/descendants`, + organizations: `${serverHostWithOrgPath}/api/server/v1`, + rootOrganization: `${serverHost}/api/server/v1`, + usersSuperOrganization: `${serverHostWithOrgPath}/api/users/v1/me/organizations/root` + }; +}; diff --git a/apps/console/src/features/organization-discovery/configs/index.ts b/apps/console/src/features/organization-discovery/configs/index.ts new file mode 100644 index 00000000000..0ff278cd2eb --- /dev/null +++ b/apps/console/src/features/organization-discovery/configs/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./endpoints"; +export * from "./ui"; diff --git a/apps/console/src/features/organization-discovery/configs/ui.ts b/apps/console/src/features/organization-discovery/configs/ui.ts new file mode 100644 index 00000000000..eb682fe317a --- /dev/null +++ b/apps/console/src/features/organization-discovery/configs/ui.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FunctionComponent, SVGProps } from "react"; +import { + ReactComponent as LDAPOutlineIcon +} from "../../../themes/default/assets/images/icons/outline-icons/ldap-outline.svg"; + +export const OrganizationIcon: FunctionComponent> = LDAPOutlineIcon; diff --git a/apps/console/src/features/organization-discovery/constants/index.ts b/apps/console/src/features/organization-discovery/constants/index.ts new file mode 100644 index 00000000000..0b914575325 --- /dev/null +++ b/apps/console/src/features/organization-discovery/constants/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./organization-constants"; diff --git a/apps/console/src/features/organization-discovery/constants/organization-constants.ts b/apps/console/src/features/organization-discovery/constants/organization-constants.ts new file mode 100644 index 00000000000..6623370846b --- /dev/null +++ b/apps/console/src/features/organization-discovery/constants/organization-constants.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class OrganizationManagementConstants { + /** + * Set of keys used to enable/disable features. + */ + public static readonly FEATURE_DICTIONARY: Map = new Map() + .set("ORGANIZATION_EMAIL_DOMAIN_CREATE", "organizationDiscovery.create") + .set("ORGANIZATION_EMAIL_DOMAIN_UPDATE", "organizationDiscovery.update") + .set("ORGANIZATION_EMAIL_DOMAIN_DELETE", "organizationDiscovery.delete") + .set("ORGANIZATION_EMAIL_DOMAIN_READ", "organizationDiscovery.read"); + +} diff --git a/apps/console/src/features/organization-discovery/models/endpoints.ts b/apps/console/src/features/organization-discovery/models/endpoints.ts new file mode 100644 index 00000000000..79cfd9a205b --- /dev/null +++ b/apps/console/src/features/organization-discovery/models/endpoints.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Interface for the Organization Management feature resource endpoints. + */ +export interface OrganizationResourceEndpointsInterface { + organizations: string; + rootOrganization: string; + usersSuperOrganization: string; + breadcrumb: string; +} diff --git a/apps/console/src/features/organization-discovery/models/index.ts b/apps/console/src/features/organization-discovery/models/index.ts new file mode 100644 index 00000000000..0a69e02025f --- /dev/null +++ b/apps/console/src/features/organization-discovery/models/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./endpoints"; +export * from "./organization-discovery"; diff --git a/apps/console/src/features/organization-discovery/models/organization-discovery.ts b/apps/console/src/features/organization-discovery/models/organization-discovery.ts new file mode 100644 index 00000000000..17634201121 --- /dev/null +++ b/apps/console/src/features/organization-discovery/models/organization-discovery.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface OrganizationInterface { + id: string; + name: string; + ref: string; + status: "ACTIVE" | "DISABLED" +} + +export interface OrganizationDiscoveryConfigInterface { + properties: OrganizationDiscoveryConfigPropertyInterface[]; +} + +export interface OrganizationDiscoveryInterface { + organizationId: string; + organizationName: string; + attributes: OrganizationDiscoveryAttributesInterface[]; +} + +export interface OrganizationDiscoveryAttributeDataInterface { + attributes: OrganizationDiscoveryAttributesInterface[]; +} + +export interface OrganizationLinkInterface { + href: string; + rel: string; +} + +export interface OrganizationListInterface { + links: OrganizationLinkInterface[]; + organizations: OrganizationInterface[]; +} + +export interface OrganizationListWithDiscoveryInterface { + links: OrganizationLinkInterface[]; + organizations: OrganizationDiscoveryInterface[]; +} + +export interface OrganizationAttributesInterface { + key: string; + value: string; +} + +export interface OrganizationDiscoveryConfigPropertyInterface { + key: string; + value: boolean; +} + +export interface OrganizationDiscoveryAttributesInterface { + type: string; + values: Array; +} + +export interface OrganizationResponseInterface { + id: string; + name: string; + description: string; + status: string; + created: string; + lastModified: string; + type: string; + domain: string; + parent: { + id: string; + ref: string; + }; + attributes: OrganizationAttributesInterface[]; +} diff --git a/apps/console/src/features/organization-discovery/pages/email-domain-discovery.tsx b/apps/console/src/features/organization-discovery/pages/email-domain-discovery.tsx new file mode 100644 index 00000000000..d15b345f71c --- /dev/null +++ b/apps/console/src/features/organization-discovery/pages/email-domain-discovery.tsx @@ -0,0 +1,540 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AccessControlConstants, Show } from "@wso2is/access-control"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { I18n } from "@wso2is/i18n"; +import { ListLayout, PageLayout, PrimaryButton } from "@wso2is/react-components"; +import { AxiosError } from "axios"; +import find from "lodash-es/find"; +import isEmpty from "lodash-es/isEmpty"; +import React, { + FunctionComponent, + MouseEvent, + ReactElement, + ReactNode, + SyntheticEvent, + useCallback, + useEffect, + useMemo, + useState +} from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import { + Checkbox, + CheckboxProps, + Divider, + DropdownItemProps, + DropdownProps, + Icon, + PaginationProps +} from "semantic-ui-react"; +import { AdvancedSearchWithBasicFilters, AppConstants, EventPublisher, UIConstants, history } from "../../core"; +import { addOrganizationDiscoveryConfig, + deleteOrganizationDiscoveryConfig, + getOrganizationDiscovery, + getOrganizationDiscoveryConfig +} from "../api"; +import { OrganizationListWithDiscovery } from "../components"; +import { + OrganizationDiscoveryConfigInterface, + OrganizationDiscoveryConfigPropertyInterface, + OrganizationListWithDiscoveryInterface +} from "../models"; + +const ORGANIZATIONS_LIST_SORTING_OPTIONS: DropdownItemProps[] = [ + { + key: 0, + text: I18n.instance.t("console:manage.features.organizationDiscovery.advancedSearch." + + "form.dropdown.filterAttributeOptions.organizationName") as ReactNode, + value: "organizationName" + } +]; + +/** + * Props for the Email Domain Discovery page. + */ +type EmailDomainDiscoveryPageInterface = IdentifiableComponentInterface; + +/** + * Email Domain Discovery page. + * + * @param props - Props injected to the component. + * @returns Email Domain Discovery page component. + */ +const EmailDomainDiscoveryPage: FunctionComponent = ( + props: EmailDomainDiscoveryPageInterface +): ReactElement => { + const { [ "data-componentid" ]: testId } = props; + + const { t } = useTranslation(); + + const dispatch: Dispatch = useDispatch(); + + const [ organizationList, setOrganizationList ] = useState(null); + const [ searchQuery, setSearchQuery ] = useState(""); + const [ listSortingStrategy, setListSortingStrategy ] = useState( + ORGANIZATIONS_LIST_SORTING_OPTIONS[ 0 ] + ); + const [ listItemLimit, setListItemLimit ] = useState(UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT); + const [ listOffset, setListOffset ] = useState(0); + const [ isOrganizationListRequestLoading, setOrganizationListRequestLoading ] = useState(true); + const [ organizationDiscoveryEnabled, setOrganizationDiscoveryEnabled ] = useState(false); + const [ triggerClearQuery, setTriggerClearQuery ] = useState(false); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + const filterQuery: string = useMemo(() => { + let filterQuery: string = ""; + + filterQuery = searchQuery; + + return filterQuery; + }, [ searchQuery ]); + + /** + * Retrieves the list of organizations. + * + * @param limit - List limit. + * @param offset - List offset. + * @param filter - Search query. + */ + const getOrganizationListWithDiscovery: ( + limit?: number, + offset?: number, + filter?: string + ) => void = useCallback( + (limit?: number, offset?: number, filter?: string): void => { + setOrganizationListRequestLoading(true); + getOrganizationDiscovery(filter, offset, limit) + .then((response: OrganizationListWithDiscoveryInterface) => { + setOrganizationList(response); + }) + .catch((error: any) => { + if (error?.description) { + dispatch( + addAlert({ + description: error.description, + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "getOrganizationListWithDiscovery.error.message" + ) + }) + ); + + return; + } + + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "getOrganizationListWithDiscovery.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "getOrganizationListWithDiscovery.genericError.message" + ) + }) + ); + }) + .finally(() => { + setOrganizationListRequestLoading(false); + }); + }, + [ getOrganizationDiscovery, dispatch, t, setOrganizationList, setOrganizationListRequestLoading ] + ); + + useEffect(() => { + getOrganizationListWithDiscovery(listItemLimit, listOffset, filterQuery); + }, [ listItemLimit, getOrganizationListWithDiscovery, listOffset, filterQuery ]); + + /** + * Sets the list sorting strategy. + * + * @param event - The event. + * @param data - Dropdown data. + */ + const handleListSortingStrategyOnChange = (event: SyntheticEvent, data: DropdownProps): void => { + setListSortingStrategy( + find(ORGANIZATIONS_LIST_SORTING_OPTIONS, (option: DropdownItemProps) => { + return data.value === option.value; + }) + ); + }; + + /** + * Handles the `onFilter` callback action from the + * organization search component. + * + * @param query - Search query. + */ + const handleOrganizationFilter: ( + query: string + ) => void = useCallback((query: string): void => { + setSearchQuery(query); + }, [ setSearchQuery ]); + + /** + * Handles the pagination change. + * + * @param event - Mouse event. + * @param data - Pagination component data. + */ + const handlePaginationChange: ( + event: MouseEvent, + data: PaginationProps + ) => void = useCallback(( + event: MouseEvent, + data: PaginationProps + ): void => { + const offsetValue: number = (data.activePage as number - 1) * listItemLimit; + + setListOffset(offsetValue); + getOrganizationListWithDiscovery(listItemLimit, listOffset, filterQuery); + }, [ getOrganizationListWithDiscovery, filterQuery, listOffset, listItemLimit ]); + + /** + * Handles per page dropdown page. + * + * @param event - Mouse event. + * @param data - Dropdown data. + */ + const handleItemsPerPageDropdownChange: ( + event: MouseEvent, + data: DropdownProps + ) => void = useCallback(( + event: MouseEvent, + data: DropdownProps + ): void => { + setListItemLimit(data.value as number); + }, [ setListItemLimit ]); + + /** + * Handles the `onSearchQueryClear` callback action. + */ + const handleSearchQueryClear: () => void = useCallback((): void => { + setTriggerClearQuery(!triggerClearQuery); + setSearchQuery(""); + }, [ setSearchQuery, triggerClearQuery ]); + + /** + * Update organization discovery enabled state based on existing data. + */ + useEffect(() => { + getOrganizationDiscoveryConfig() + .then((response: OrganizationDiscoveryConfigInterface) => { + response?.properties?.forEach((property:OrganizationDiscoveryConfigPropertyInterface) => { + if (property.key === "emailDomain.enable") { + setOrganizationDiscoveryEnabled(property.value); + } + }); + }) + .catch((error: AxiosError) => { + if (error.response.status == 404) { + setOrganizationDiscoveryEnabled(false); + + return; + } + if (error.response && error.response.data && error.response.data.detail) { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "getEmailDomainDiscovery.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "getEmailDomainDiscovery.error.message" + ) + }) + ); + } else { + // Generic error message + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "getEmailDomainDiscovery.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "getEmailDomainDiscovery.genericError.message" + ) + }) + ); + } + }); + }, []); + + /** + * This is called when the enable toggle changes. + * + * @param e - Event object + * @param data - The data object. + */ + const handleToggle = (e: SyntheticEvent, data: CheckboxProps): void => { + + setOrganizationDiscoveryEnabled(data.checked); + + if (data.checked == true) { + + const updateData: OrganizationDiscoveryConfigInterface = { + properties: [] + }; + + updateData.properties.push({ + key: "emailDomain.enable", + value: true + }); + + addOrganizationDiscoveryConfig(updateData) + .then(() => { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.success.message" + ) + }) + ); + }) + .catch((error: AxiosError) => { + if (error.response && error.response.data && error.response.data.detail) { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.error.message" + ) + }) + ); + } else { + // Generic error message + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "enableEmailDomainDiscovery.genericError.message" + ) + }) + ); + } + }); + + return; + } + + deleteOrganizationDiscoveryConfig() + .then(() => { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.success.message" + ) + }) + ); + }) + .catch((error: AxiosError) => { + if (error.response && error.response.data && error.response.data.detail) { + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.error.message" + ) + }) + ); + } else { + // Generic error message + dispatch( + addAlert({ + description: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizationDiscovery.notifications." + + "disableEmailDomainDiscovery.genericError.message" + ) + }) + ); + } + }); + }; + + /** + * This renders the enable toggle. + */ + const discoveryToggle = (): ReactElement => { + return ( + + ); + }; + + return ( + <> + + { + eventPublisher.publish("organization-click-assign-email-domain-button"); + history.push(AppConstants.getPaths().get("EMAIL_DOMAIN_ASSIGN")); + } } + data-componentid={ `${ testId }-list-layout-assign-button` } + > + + { t("console:manage.features.organizationDiscovery.emailDomains.actions.assign") } + + + ) + } + pageTitle={ t("console:manage.pages.emailDomainDiscovery.title") } + title={ t("console:manage.pages.emailDomainDiscovery.title") } + description={ t("console:manage.pages.emailDomainDiscovery.subTitle") } + data-componentid={ `${ testId }-page-layout` } + > + { discoveryToggle() } + + + ); +}; + +/** + * Default props for the component. + */ +EmailDomainDiscoveryPage.defaultProps = { + "data-componentid": "email-domain-discovery-page" +}; + +/** + * A default export was added to support React.lazy. + * TODO: Change this to a named export once react starts supporting named exports for code splitting. + * @see {@link https://reactjs.org/docs/code-splitting.html#reactlazy} + */ +export default EmailDomainDiscoveryPage; diff --git a/apps/console/src/features/organization-discovery/pages/email-domain-edit.tsx b/apps/console/src/features/organization-discovery/pages/email-domain-edit.tsx new file mode 100644 index 00000000000..f8023803c69 --- /dev/null +++ b/apps/console/src/features/organization-discovery/pages/email-domain-edit.tsx @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isFeatureEnabled } from "@wso2is/core/helpers"; +import { AlertLevels, SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { GenericIcon, PageLayout } from "@wso2is/react-components"; +import React, { FunctionComponent, ReactElement, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { RouteChildrenProps } from "react-router-dom"; +import { Dispatch } from "redux"; +import { AppConstants, FeatureConfigInterface, history } from "../../core"; +import { getOrganization, getOrganizationDiscoveryAttributes } from "../api"; +import { EditOrganizationEmailDomains } from "../components/edit-organization/edit-organization-email-domains"; +import { OrganizationIcon } from "../configs"; +import { OrganizationManagementConstants } from "../constants"; +import { OrganizationDiscoveryAttributeDataInterface, OrganizationResponseInterface } from "../models"; + +interface OrganizationEmailDomainEditPagePropsInterface extends SBACInterface, + TestableComponentInterface, RouteChildrenProps{ +} + +const OrganizationEmailDomainEditPage: FunctionComponent = ( + props: OrganizationEmailDomainEditPagePropsInterface +): ReactElement => { + + const { + featureConfig, + location + } = props; + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ organization, setOrganization ] = useState(); + const [ organizationDiscoveryData, setOrganizationDiscoveryData ] = useState + (); + const [ isReadOnly, setIsReadOnly ] = useState(true); + + + useEffect(() => { + setIsReadOnly( + !isFeatureEnabled( + featureConfig?.organizationDiscovery, + OrganizationManagementConstants.FEATURE_DICTIONARY.get("ORGANIZATION_EMAIL_DOMAIN_UPDATE") + )); + }, [ featureConfig, organization ]); + + const getOrganizationData: (organizationId: string) => void = useCallback((organizationId: string): void => { + getOrganization(organizationId) + .then((organization: OrganizationResponseInterface) => { + setOrganization(organization); + }).catch((error: any) => { + if (error?.description) { + dispatch(addAlert({ + description: error.description, + level: AlertLevels.ERROR, + message: t("console:manage.features.organizations.notifications.fetchOrganization." + + "genericError.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("console:manage.features.organizations.notifications.fetchOrganization." + + "genericError.description"), + level: AlertLevels.ERROR, + message: t("console:manage.features.organizations.notifications.fetchOrganization." + + "genericError.message") + })); + }); + }, [ dispatch, t ]); + + const getOrganizationDiscoveryData: (organizationId: string) => void = useCallback( + (organizationId: string): void => { + getOrganizationDiscoveryAttributes(organizationId) + .then((organizationDiscoveryData: OrganizationDiscoveryAttributeDataInterface) => { + setOrganizationDiscoveryData(organizationDiscoveryData); + }).catch((error: any) => { + if (error?.description) { + dispatch(addAlert({ + description: error.description, + level: AlertLevels.ERROR, + message: t("console:manage.features.organizationDiscovery.notifications." + + "fetchOrganizationDiscoveryAttributes.genericError.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("console:manage.features.organizationDiscovery.notifications." + + "fetchOrganizationDiscoveryAttributes.genericError.description"), + level: AlertLevels.ERROR, + message: t("console:manage.features.organizationDiscovery.notifications." + + "fetchOrganizationDiscoveryAttributes.genericError.message") + })); + }); + }, [ dispatch, t ]); + + useEffect(() => { + const path: string[] = location.pathname.split("/"); + const id: string = path[path.length - 1]; + + getOrganizationData(id); + getOrganizationDiscoveryData(id); + }, [ location, getOrganizationData, getOrganizationDiscoveryData ]); + + const goBackToOrganizationListWithDomains: () => void = useCallback(() => + history.push(AppConstants.getPaths().get("EMAIL_DOMAIN_DISCOVERY")),[ history ] + ); + + + return ( + + ) } + backButton={ { + "data-testid": "org-email-domains-edit-org-back-button", + onClick: goBackToOrganizationListWithDomains, + text: t("console:manage.features.organizationDiscovery.edit.back") + } } + titleTextAlign="left" + bottomMargin={ false } + > + + + ); +}; + +export default OrganizationEmailDomainEditPage; diff --git a/apps/console/src/features/organizations/pages/organization-edit.tsx b/apps/console/src/features/organizations/pages/organization-edit.tsx index 167691b5d2d..788c9141c6d 100644 --- a/apps/console/src/features/organizations/pages/organization-edit.tsx +++ b/apps/console/src/features/organizations/pages/organization-edit.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json index 90ef2d97b33..6b0af136d19 100644 --- a/apps/console/src/public/deployment.config.json +++ b/apps/console/src/public/deployment.config.json @@ -680,6 +680,27 @@ }, "disabledFeatures": [] }, + "organizationDiscovery": { + "enabled": true, + "scopes": { + "feature": [ + "console:organizationDiscovery" + ], + "create": [ + "internal_organization_create" + ], + "delete": [ + "internal_organization_delete" + ], + "read": [ + "internal_organization_view" + ], + "update": [ + "internal_organization_update" + ] + }, + "disabledFeatures": [] + }, "manageGettingStarted": { "disabledFeatures": [], "enabled": true, diff --git a/modules/common/src/constants/app-constants.ts b/modules/common/src/constants/app-constants.ts index ab1a3afdbb8..db8e8e41e90 100644 --- a/modules/common/src/constants/app-constants.ts +++ b/modules/common/src/constants/app-constants.ts @@ -1,10 +1,19 @@ -/** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * - * This software is the property of WSO2 LLC. and its suppliers, if any. - * Dissemination of any information or reproduction of any material contained - * herein in any form is strictly forbidden, unless permitted by WSO2 expressly. - * You may not alter or remove any copyright or other notice from copies of this content. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import { AppThemeConfigInterface } from "@wso2is/core/models"; @@ -244,6 +253,9 @@ export class AppConstants { .set("CONNECTION_EDIT", `${ AppConstants.getDeveloperViewBasePath() }/connections/:id`) .set("CUSTOMIZE", `${ AppConstants.getMainViewBasePath() }/customize`) .set("DEVELOPER_OVERVIEW", `${ AppConstants.getDeveloperViewBasePath() }/overview`) + .set("EMAIL_DOMAIN_ASSIGN", `${ AppConstants.getAdminViewBasePath() }/email-domain-assign`) + .set("EMAIL_DOMAIN_DISCOVERY", `${ AppConstants.getAdminViewBasePath() }/email-domain-discovery`) + .set("EMAIL_DOMAIN_UPDATE", `${ AppConstants.getAdminViewBasePath() }/email-domain-edit/:id`) .set("EMAIL_PROVIDER", `${ AppConstants.getDeveloperViewBasePath() }/email-provider`) .set("EMAIL_TEMPLATE_TYPES", `${ AppConstants.getAdminViewBasePath() }/email-templates`) .set("EMAIL_TEMPLATES", `${ AppConstants.getAdminViewBasePath() }/email-templates/:templateTypeId`) diff --git a/modules/i18n/src/models/namespaces/console-ns.ts b/modules/i18n/src/models/namespaces/console-ns.ts index 217b3884ccd..f12193a3cd3 100644 --- a/modules/i18n/src/models/namespaces/console-ns.ts +++ b/modules/i18n/src/models/namespaces/console-ns.ts @@ -3243,6 +3243,55 @@ export interface ConsoleNS { groupName: FormAttributes; }; }; + organizationDiscovery: { + advancedSearch: { + form: { + dropdown: { + filterAttributeOptions: { + organizationName: string; + }; + }; + inputs: { + filterAttribute: { + placeholder: string; + }; + filterCondition: { + placeholder: string; + }; + filterValue: { + placeholder: string; + }; + }; + }; + placeholder: string; + }; + emailDomains: { + actions: { + assign: string; + enable: string; + } + }; + edit: { + back: string; + description: string; + fields: { + name: FormAttributes; + emailDomains: FormAttributes; + } + }; + notifications: { + disableEmailDomainDiscovery: Notification; + enableEmailDomainDiscovery: Notification; + fetchOrganizationDiscoveryAttributes: Notification; + getEmailDomainDiscovery: Notification; + getOrganizationListWithDiscovery: Notification; + updateOrganizationDiscoveryAttributes: Notification; + }, + placeholders: { + emptyList: Placeholder; + }; + title: string; + }; organizations: { advancedSearch: { form: { @@ -5426,6 +5475,7 @@ export interface ConsoleNS { editRoles: string; editUsers: string; editUserstore: string; + emailDomainDiscovery: string; emailTemplateTypes: string; emailTemplates: string; generalConfigurations: string; @@ -6220,6 +6270,7 @@ export interface ConsoleNS { addEmailTemplate: EditPage; approvalsPage: Page; editTemplate: EditPage; + emailDomainDiscovery: Page; emailLocaleAdd: EditPage; emailLocaleAddWithDisplayName: EditPage; emailTemplateTypes: Page; diff --git a/modules/i18n/src/translations/en-US/portals/console.ts b/modules/i18n/src/translations/en-US/portals/console.ts index a95ed69df46..2422e4f167b 100644 --- a/modules/i18n/src/translations/en-US/portals/console.ts +++ b/modules/i18n/src/translations/en-US/portals/console.ts @@ -9310,6 +9310,130 @@ export const console: ConsoleNS = { } } }, + organizationDiscovery: { + advancedSearch: { + form: { + dropdown: { + filterAttributeOptions: { + organizationName: "Organization Name" + } + }, + inputs: { + filterAttribute: { + placeholder: "E.g. Organization Name etc." + }, + filterCondition: { + placeholder: "E.g. Starts with etc." + }, + filterValue: { + placeholder: "Enter value to search" + } + } + }, + placeholder: "Search by Organization Name" + }, + emailDomains: { + actions: { + assign: "Assign Email Domain", + enable: "Enable email domain discovery" + } + }, + edit: { + back: "Back", + description: "Edit Email Domains", + fields: { + name: { + label: "Organization Name" + }, + emailDomains: { + label : "Email Domains", + placeHolder: "Enter email domains" + } + } + }, + notifications: { + disableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Error while disabling email domain discovery" + }, + genericError: { + description: "An error occurred while disabling email domain discovery", + message: "Something went wrong" + }, + success: { + description: "Successfully disabled email domain discovery", + message: "Email domain discovery disabled successfully" + } + }, + enableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Error while enabling email domain discovery" + }, + genericError: { + description: "An error occurred while enabling email domain discovery", + message: "Something went wrong" + }, + success: { + description: "Successfully enabled email domain discovery", + message: "Email domain discovery enabled successfully" + } + }, + fetchOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "Error while fetching the organization discovery attributes" + }, + genericError: { + description: "An error occurred while fetching the organization discovery attributes", + message: "Something went wrong" + } + }, + getEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Error while retrieving email domain discovery configuration" + }, + genericError: { + description: "An error occurred while retrieving email domain discovery configuration", + message: "Something went wrong" + } + }, + getOrganizationListWithDiscovery: { + error: { + description: "{{description}}", + message: "Error while getting the organization list with discovery attributes" + }, + genericError: { + description: "An error occurred while getting the organization list with discovery attributes", + message: "Something went wrong" + } + }, + updateOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "Error while updating the organization discovery attributes" + }, + genericError: { + description: "An error occurred while updating the organization discovery attributes", + message: "Something went wrong" + }, + success: { + description: "Successfully updated the organization discovery attributes", + message: "Organization discovery attributes updated successfully" + } + } + }, + placeholders: { + emptyList: { + action: "Assign Email Domain", + subtitles: "There are no organizations with email domains assigned.", + title: "Assign Email Domain" + } + }, + title: "Email Domain Discovery" + }, organizations: { advancedSearch: { form: { @@ -10520,6 +10644,7 @@ export const console: ConsoleNS = { editRoles: "Edit Role", editUsers: "Edit User", editUserstore: "Edit User Store", + emailDomainDiscovery: "Email Domain Discovery", emailTemplateTypes: "", emailTemplates: "Email Templates", generalConfigurations: "General", @@ -12091,6 +12216,10 @@ export const console: ConsoleNS = { subTitle: null, title: "{{template}}" }, + emailDomainDiscovery: { + subTitle: "Configure email domain discovery for organizations.", + title: "Email Domain Discovery" + }, emailLocaleAdd: { backButton: "Go back to {{name}} template", subTitle: null, diff --git a/modules/i18n/src/translations/fr-FR/portals/console.ts b/modules/i18n/src/translations/fr-FR/portals/console.ts index a9722a03f24..01adf79658b 100755 --- a/modules/i18n/src/translations/fr-FR/portals/console.ts +++ b/modules/i18n/src/translations/fr-FR/portals/console.ts @@ -7539,6 +7539,130 @@ export const console: ConsoleNS = { } } }, + organizationDiscovery: { + advancedSearch: { + form: { + dropdown: { + filterAttributeOptions: { + organizationName: "nom de l'organisation" + } + }, + inputs: { + filterAttribute: { + placeholder: "Par exemple. Nom de l'organisation, etc." + }, + filterCondition: { + placeholder: "Par exemple. Commence par etc." + }, + filterValue: { + placeholder: "Entrez la valeur à rechercher" + } + } + }, + placeholder: "Rechercher par nom d'organisation" + }, + emailDomains: { + actions: { + assign: "Attribuer un domaine de messagerie", + enable: "Activer la découverte de domaines de messagerie" + } + }, + edit: { + back: "Dos", + description: "Modifier les domaines de messagerie", + fields: { + name: { + label: "nom de l'organisation" + }, + emailDomains: { + label: "Domaines de messagerie", + placeHolder: "Entrez les domaines de messagerie" + } + } + }, + notifications: { + disableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Erreur lors de la désactivation de la découverte de domaine de messagerie" + }, + genericError: { + description: "Une erreur s'est produite lors de la désactivation de la découverte de domaines de messagerie", + message: "Quelque chose s'est mal passé" + }, + success: { + description: "La découverte du domaine de messagerie a été désactivée avec succès", + message: "La découverte du domaine de messagerie a été désactivée avec succès" + } + }, + enableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Erreur lors de l'activation de la découverte de domaines de messagerie" + }, + genericError: { + description: "Une erreur s'est produite lors de l'activation de la découverte de domaines de messagerie", + message: "Quelque chose s'est mal passé" + }, + success: { + description: "La découverte du domaine de messagerie a été activée avec succès", + message: "La découverte du domaine de messagerie a été activée avec succès" + } + }, + fetchOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "Erreur lors de la récupération des attributs de découverte de l'organisation" + }, + genericError: { + description: "Une erreur s'est produite lors de la récupération des attributs de découverte de l'organisation", + message: "Quelque chose s'est mal passé" + } + }, + getEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "Erreur lors de la récupération de la configuration de la découverte du domaine de messagerie" + }, + genericError: { + description: "Une erreur s'est produite lors de la récupération de la configuration de la découverte du domaine de messagerie", + message: "Quelque chose s'est mal passé" + } + }, + getOrganizationListWithDiscovery: { + error: { + description: "{{description}}", + message: "Erreur lors de l'obtention de la liste des organisations avec les attributs de découverte" + }, + genericError: { + description: "Une erreur s'est produite lors de l'obtention de la liste des organisations avec les attributs de découverte", + message: "Quelque chose s'est mal passé" + } + }, + updateOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "Erreur lors de la mise à jour des attributs de découverte de l'organisation" + }, + genericError: { + description: "Une erreur s'est produite lors de la mise à jour des attributs de découverte de l'organisation", + message: "Quelque chose s'est mal passé" + }, + success: { + description: "Mise à jour réussie des attributs de découverte de l'organisation", + message: "Attributs de découverte d'organisation mis à jour avec succès" + } + } + }, + placeholders: { + emptyList: { + action: "Attribuer un domaine de messagerie", + subtitles: "Aucune organisation ne dispose de domaines de messagerie attribués.", + title: "Attribuer un domaine de messagerie" + } + }, + title: "Découverte de domaines de messagerie" + }, organizations: { advancedSearch: { form: { @@ -8781,6 +8905,7 @@ export const console: ConsoleNS = { editRoles: "Modifier le rôle", editUsers: "Modifier l'utilisateur", editUserstore: "Modifier l'annuaire", + emailDomainDiscovery: "Découverte de domaines de messagerie", emailTemplateTypes: "", emailTemplates: "Modèles d'e-mail", generalConfigurations: "Général", @@ -10395,6 +10520,10 @@ export const console: ConsoleNS = { subTitle: null, title: "{{template}}" }, + emailDomainDiscovery: { + subTitle: "Configurez la découverte de domaines de messagerie pour les organisations.", + title: "Découverte de domaines de messagerie" + }, emailLocaleAdd: { backButton: "Revenir au modèle {{name}}", subTitle: null, diff --git a/modules/i18n/src/translations/si-LK/portals/console.ts b/modules/i18n/src/translations/si-LK/portals/console.ts index cacdb26c10e..13ebb705307 100755 --- a/modules/i18n/src/translations/si-LK/portals/console.ts +++ b/modules/i18n/src/translations/si-LK/portals/console.ts @@ -7381,6 +7381,130 @@ export const console: ConsoleNS = { } } }, + organizationDiscovery: { + advancedSearch: { + form: { + dropdown: { + filterAttributeOptions: { + organizationName: "සංවිධානයේ නම" + } + }, + inputs: { + filterAttribute: { + placeholder: "උදා. සංවිධානයේ නම ආදිය." + }, + filterCondition: { + placeholder: "උදා. ආදියෙන් ආරම්භ වේ." + }, + filterValue: { + placeholder: "සෙවීමට අගය ඇතුළත් කරන්න" + } + } + }, + placeholder: "සංවිධානයේ නම අනුව සොයන්න" + }, + emailDomains: { + actions: { + assign: "විද්‍යුත් තැපැල් වසම පවරන්න", + enable: "ඊමේල් වසම් සොයාගැනීම සබල කරන්න" + } + }, + edit: { + back: "ආපසු", + description: "ඊමේල් වසම් සංස්කරණය කරන්න", + fields: { + name: { + label: "සංවිධානයේ නම" + }, + emailDomains: { + label : "ඊමේල් වසම්", + placeHolder: "ඊමේල් වසම් ඇතුලත් කරන්න" + } + } + }, + notifications: { + disableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "විද්‍යුත් තැපැල් වසම් සොයාගැනීම අක්‍රිය කිරීමේදී දෝෂයකි" + }, + genericError: { + description: "විද්‍යුත් තැපැල් වසම් සොයාගැනීම අක්‍රිය කිරීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + }, + success: { + description: "විද්‍යුත් තැපැල් වසම් සොයා ගැනීම සාර්ථකව අක්‍රීය කරන ලදී", + message: "විද්‍යුත් තැපෑල වසම් සොයා ගැනීම සාර්ථකව අක්‍රීය කර ඇත" + } + }, + enableEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "විද්‍යුත් තැපැල් වසම් සොයාගැනීම සබල කිරීමේදී දෝෂයකි" + }, + genericError: { + description: "විද්‍යුත් තැපෑල වසම් සොයාගැනීම සබල කිරීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + }, + success: { + description: "විද්‍යුත් තැපැල් වසම් සොයාගැනීම සාර්ථකව සක්‍රීය කර ඇත", + message: "විද්‍යුත් තැපැල් වසම් සොයා ගැනීම සාර්ථකව සක්‍රීය කර ඇත" + } + }, + fetchOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "සංවිධානයේ සොයාගැනීම් ගුණාංග ලබා ගැනීමේදී දෝෂයකි" + }, + genericError: { + description: "සංවිධානයේ සොයාගැනීම් ගුණාංග ලබා ගැනීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + } + }, + getEmailDomainDiscovery: { + error: { + description: "{{description}}", + message: "විද්‍යුත් තැපැල් වසම් සොයාගැනීමේ වින්‍යාසය ලබා ගැනීමේදී දෝෂයකි" + }, + genericError: { + description: "ඊමේල් වසම් සොයාගැනීමේ වින්‍යාසය ලබා ගැනීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + } + }, + getOrganizationListWithDiscovery: { + error: { + description: "{{description}}", + message: "සොයාගැනීම් ගුණාංග සහිත සංවිධාන ලැයිස්තුව ලබා ගැනීමේදී දෝෂයකි" + }, + genericError: { + description: "සොයාගැනීම් ගුණාංග සහිත සංවිධාන ලැයිස්තුව ලබා ගැනීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + } + }, + updateOrganizationDiscoveryAttributes: { + error: { + description: "{{description}}", + message: "සංවිධානයේ සොයාගැනීම් ගුණාංග යාවත්කාලීන කිරීමේදී දෝෂයකි" + }, + genericError: { + description: "සංවිධානයේ සොයාගැනීම් ගුණාංග යාවත්කාලීන කිරීමේදී දෝෂයක් ඇති විය", + message: "මොකක්හරි වැරැද්දක් වෙලා" + }, + success: { + description: "සංවිධානයේ සොයාගැනීම් උපලක්ෂණ සාර්ථකව යාවත්කාලීන කරන ලදී", + message: "සංවිධානයේ සොයාගැනීම් උපලක්ෂණ සාර්ථකව යාවත්කාලීන කරන ලදී" + } + } + }, + placeholders: { + emptyList: { + action: "විද්‍යුත් තැපැල් වසම පවරන්න", + subtitles: "පවරන ලද ඊමේල් වසම් සහිත සංවිධාන නොමැත.", + title: "විද්‍යුත් තැපැල් වසම පවරන්න" + } + }, + title: "ඊමේල් වසම් සොයාගැනීම" + }, organizations: { advancedSearch: { form: { @@ -8588,6 +8712,7 @@ export const console: ConsoleNS = { editRoles: "භූමිකාව සංස්කරණය කරන්න", editUsers: "පරිශීලක සංස්කරණය කරන්න", editUserstore: "පරිශීලක වෙළඳසැල සංස්කරණය කරන්න", + emailDomainDiscovery: "ඊමේල් වසම් සොයාගැනීම", emailTemplateTypes: "", emailTemplates: "විද්‍යුත් තැපැල් ආකෘති", generalConfigurations: "ජනරාල්", @@ -10163,6 +10288,10 @@ export const console: ConsoleNS = { subTitle: null, title: "{{template}}" }, + emailDomainDiscovery: { + subTitle: "ආයතන සඳහා විද්‍යුත් තැපැල් වසම් සොයාගැනීම වින්‍යාස කරන්න..", + title: "ඊමේල් වසම් සොයාගැනීම" + }, emailLocaleAdd: { backButton: "{{name}} අච්චුව වෙත ආපසු යන්න", subTitle: null,