diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/ProfilerConfiguration.constant.ts b/openmetadata-ui/src/main/resources/ui/cypress/constants/ProfilerConfiguration.constant.ts new file mode 100644 index 00000000000..1d70b6da921 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/ProfilerConfiguration.constant.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Collate. + * Licensed 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 const PROFILER_REQUEST_CONFIG = { + config_type: 'profilerConfiguration', + config_value: { + metricConfiguration: [ + { + dataType: 'AGG_STATE', + metrics: [ + 'COLUMN_COUNT', + 'COLUMN_NAMES', + 'COUNT', + 'COUNT_IN_SET', + 'DISTINCT_COUNT', + 'DISTINCT_RATIO', + 'DUPLICATE_COUNT', + 'FIRST_QUARTILE', + 'HISTOGRAM', + 'ILIKE_COUNT', + 'ILIKE_RATIO', + 'IQR', + 'LIKE_COUNT', + 'LIKE_RATIO', + 'MAX', + 'MAX_LENGTH', + 'MEAN', + 'MEDIAN', + 'MIN', + 'MIN_LENGTH', + 'NON_PARAMETRIC_SKEW', + 'NOT_LIKE_COUNT', + 'NOT_REGEX_COUNT', + 'NULL_COUNT', + 'NULL_RATIO', + 'REGEX_COUNT', + 'ROW_COUNT', + 'STDDEV', + 'SUM', + 'SYSTEM', + 'THIRD_QUARTILE', + 'UNIQUE_COUNT', + 'UNIQUE_RATIO', + ], + disabled: false, + }, + { + dataType: 'AGGREGATEFUNCTION', + metrics: ['COLUMN_COUNT', 'COLUMN_NAMES'], + }, + { + dataType: 'ARRAY', + metrics: [ + 'COLUMN_COUNT', + 'COLUMN_NAMES', + 'COUNT', + 'COUNT_IN_SET', + 'DISTINCT_COUNT', + 'DISTINCT_RATIO', + 'DUPLICATE_COUNT', + 'FIRST_QUARTILE', + 'HISTOGRAM', + 'ILIKE_COUNT', + 'ILIKE_RATIO', + 'IQR', + 'LIKE_COUNT', + 'LIKE_RATIO', + 'MAX', + 'MAX_LENGTH', + 'MEAN', + 'MEDIAN', + 'MIN', + 'MIN_LENGTH', + 'NON_PARAMETRIC_SKEW', + 'NOT_LIKE_COUNT', + 'NOT_REGEX_COUNT', + 'NULL_COUNT', + 'NULL_RATIO', + 'REGEX_COUNT', + 'ROW_COUNT', + 'STDDEV', + 'SUM', + 'SYSTEM', + 'THIRD_QUARTILE', + 'UNIQUE_COUNT', + 'UNIQUE_RATIO', + ], + disabled: true, + }, + ], + }, +}; + +export const PROFILER_EMPTY_RESPONSE_CONFIG = { + config_type: 'profilerConfiguration', + config_value: { metricConfiguration: [] }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/ProfilerConfigurationPage.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/ProfilerConfigurationPage.spec.ts new file mode 100644 index 00000000000..5eb7887d3a6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/ProfilerConfigurationPage.spec.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2024 Collate. + * Licensed 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 { interceptURL } from '../../common/common'; +import { getToken } from '../../common/Utils/LocalStorage'; +import { generateRandomUser } from '../../common/Utils/Owner'; +import { SidebarItem } from '../../constants/Entity.interface'; +import { + PROFILER_EMPTY_RESPONSE_CONFIG, + PROFILER_REQUEST_CONFIG, +} from '../../constants/ProfilerConfiguration.constant'; +const visitProfilerConfigurationPage = () => { + interceptURL( + 'GET', + '/api/v1/system/settings/profilerConfiguration', + 'getProfilerConfiguration' + ); + cy.sidebarClick(SidebarItem.SETTINGS); + cy.get('[data-testid="preferences"]').scrollIntoView().click(); + cy.get('[data-testid="preferences.profiler-configuration"]') + .scrollIntoView() + .click(); + cy.wait('@getProfilerConfiguration'); +}; + +const user = generateRandomUser(); +let userId = ''; + +describe('ProfilerConfigurationPage', () => { + before(() => { + cy.login(); + cy.getAllLocalStorage().then((data) => { + const token = getToken(data); + + // Create a new user + cy.request({ + method: 'POST', + url: `/api/v1/users/signup`, + headers: { Authorization: `Bearer ${token}` }, + body: user, + }).then((response) => { + userId = response.body.id; + }); + }); + }); + + after(() => { + cy.login(); + cy.getAllLocalStorage().then((data) => { + const token = getToken(data); + + // Delete created user + cy.request({ + method: 'DELETE', + url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`, + headers: { Authorization: `Bearer ${token}` }, + }); + }); + }); + + beforeEach(() => { + cy.login(); + }); + + it('Verify validation', () => { + visitProfilerConfigurationPage(); + cy.get('[data-testid="save-button"]').click(); + cy.get('#metricConfiguration_0_dataType_help').should( + 'contain', + 'Data Type is required.' + ); + cy.get('[data-testid="cancel-button"]').click(); + cy.url().should('eq', `${Cypress.config().baseUrl}/settings/preferences`); + }); + + it('Update profiler configuration', () => { + visitProfilerConfigurationPage(); + cy.get('#metricConfiguration_0_dataType').click(); + cy.get(`[title="AGG_STATE"]`).filter(':visible').scrollIntoView().click(); + + cy.get('#metricConfiguration_0_metrics').click().type('All'); + cy.get(`[role="tree"] [title="All"]`) + .filter(':visible') + .scrollIntoView() + .click(); + + cy.get('[data-testid="add-fields"]').click(); + cy.get('#metricConfiguration_1_dataType').click(); + cy.get(`.rc-virtual-list-holder-inner [title="AGG_STATE"]`) + .filter(':visible') + .should('have.class', 'ant-select-item-option-disabled'); + cy.get(`.rc-virtual-list-holder-inner [title="AGGREGATEFUNCTION"]`) + .filter(':visible') + .scrollIntoView() + .click(); + cy.clickOutside(); + cy.get('#metricConfiguration_1_metrics').click().type('column'); + cy.get(`[role="tree"] [title="COLUMN_COUNT"]`).filter(':visible').click(); + cy.get(`[role="tree"] [title="COLUMN_NAMES"]`).filter(':visible').click(); + cy.clickOutside(); + + cy.get('[data-testid="add-fields"]').click(); + cy.get('#metricConfiguration_2_dataType').click(); + cy.get(`.rc-virtual-list-holder-inner [title="ARRAY"]`) + .filter(':visible') + .scrollIntoView() + .click(); + cy.get('#metricConfiguration_2_metrics').click().type('All'); + cy.get(`[role="tree"] [title="All"]`) + .filter(':visible') + .scrollIntoView() + .click(); + cy.clickOutside(); + cy.get('#metricConfiguration_2_disabled').click(); + interceptURL( + 'PUT', + '/api/v1/system/settings', + 'updateProfilerConfiguration' + ); + cy.get('[data-testid="save-button"]').click(); + cy.wait('@updateProfilerConfiguration').then((interception) => { + expect(interception.request.body).to.deep.eq(PROFILER_REQUEST_CONFIG); + }); + }); + + it('Remove Configuration', () => { + visitProfilerConfigurationPage(); + cy.get('[data-testid="remove-filter-2"]').click(); + cy.get('[data-testid="remove-filter-1"]').click(); + cy.get('[data-testid="remove-filter-0"]').click(); + + interceptURL( + 'PUT', + '/api/v1/system/settings', + 'updateProfilerConfiguration' + ); + cy.get('[data-testid="save-button"]').click(); + cy.wait('@updateProfilerConfiguration').then((interception) => { + expect(interception.request.body).to.deep.eq( + PROFILER_EMPTY_RESPONSE_CONFIG + ); + }); + }); + + it('permission check for non admin user', () => { + cy.logout(); + cy.login(user.email, user.password); + cy.sidebarClick(SidebarItem.SETTINGS); + cy.get('[data-testid="preferences"]').should('not.exist'); + cy.logout(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/profiler-configuration-logo.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/profiler-configuration-logo.svg new file mode 100644 index 00000000000..aeb60069b95 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/profiler-configuration-logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx index 9304a60aa4c..c41c19f6e9f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx @@ -44,6 +44,7 @@ import AddRulePage from '../../pages/PoliciesPage/PoliciesDetailPage/AddRulePage import EditRulePage from '../../pages/PoliciesPage/PoliciesDetailPage/EditRulePage'; import PoliciesDetailPage from '../../pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage'; import PoliciesListPage from '../../pages/PoliciesPage/PoliciesListPage/PoliciesListPage'; +import ProfilerConfigurationPage from '../../pages/ProfilerConfigurationPage/ProfilerConfigurationPage'; import AddRolePage from '../../pages/RolesPage/AddRolePage/AddRolePage'; import RolesDetailPage from '../../pages/RolesPage/RolesDetailPage/RolesDetailPage'; import RolesListPage from '../../pages/RolesPage/RolesListPage/RolesListPage'; @@ -288,6 +289,15 @@ const SettingsRouter = () => { GlobalSettingOptions.APPEARANCE )} /> + ({ + label: value, + key: value, + value, + })), + }, +]; + +export const DEFAULT_PROFILER_CONFIG_VALUE = { + metricConfiguration: [ + { + dataType: undefined, + metrics: undefined, + disabled: false, + }, + ], +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 741e683803b..4db272d8a7d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Metadaten-zu-ES-Konfiguration (optional)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "Metriktyp", "metric-value": "Metrikwert", "metrics-summary": "Metrikzusammenfassung", @@ -845,6 +846,7 @@ "profile-sample-type": "Profil-Sample-Typ {{type}}", "profiler": "Profiler", "profiler-amp-data-quality": "Profiler & Datenqualität", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Profiler-Erfassung", "profiler-lowercase": "profiler", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Represent different persona that a user may have withing OpenMetadata.", "page-sub-header-for-pipelines": "Ingestion von Metadaten aus den am häufigsten verwendeten Pipeline-Diensten.", "page-sub-header-for-policies": "Definiere Richtlinien mit einer Reihe von Regeln für feinkörnige Zugriffssteuerung.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Weise Benutzern oder Teams umfassende rollenbasierte Zugriffsberechtigungen zu.", "page-sub-header-for-search": "Ingestion von Metadaten aus den beliebtesten Suchdiensten.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 4f385735f4c..62c16063cd6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Metadata To ES Config (Optional)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "Metric Type", "metric-value": "Metric Value", "metrics-summary": "Metrics Summary", @@ -845,6 +846,7 @@ "profile-sample-type": "Profile Sample {{type}}", "profiler": "Profiler", "profiler-amp-data-quality": "Profiler & Data Quality", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Profiler Ingestion", "profiler-lowercase": "profiler", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Enhance and customize the user experience with Personas.", "page-sub-header-for-pipelines": "Ingest metadata from the most used pipeline services.", "page-sub-header-for-policies": "Define policies with a set of rules for fine-grained access control.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Assign comprehensive role based access to Users or Teams.", "page-sub-header-for-search": "Ingest metadata from the most popular search services.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index dfaa739c96d..0b54e05c47a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Configuración de Metadatos a ES (Opcional)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Sugerencia de Metapilot", + "metric-configuration": "Metric Configuration", "metric-type": "Tipo de Métrica", "metric-value": "Valor de Métrica", "metrics-summary": "Resumen de Métricas", @@ -845,6 +846,7 @@ "profile-sample-type": "Muestra de perfil {{type}}", "profiler": "Perfilador", "profiler-amp-data-quality": "Perfilador y calidad de datos", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Ingesta del Perfilador", "profiler-lowercase": "perfilador", "profiler-setting-plural": "Configuración del perfilador", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Representa diferentes personas que un usuario puede tener dentro de OpenMetadata.", "page-sub-header-for-pipelines": "Ingresa metadatos desde los servicios de pipeline más utilizados.", "page-sub-header-for-policies": "Define políticas con un conjunto de reglas para el control de acceso detallado.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Asigna un acceso basado en roles integral a Usuarios o Equipos.", "page-sub-header-for-search": "Ingresa metadatos desde los servicios de búsqueda más populares.", "page-sub-header-for-setting": "Capacidad para configurar la aplicación OpenMetadata según tus necesidades.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 4184c68a676..61faf205754 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Configuration de Métadonnées vers ES (Optionnelle)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "Type de Mesure", "metric-value": "Valeur de la Mesure", "metrics-summary": "Résumé des Mesures", @@ -845,6 +846,7 @@ "profile-sample-type": "Échantillon du Profil {{type}}", "profiler": "Profilage", "profiler-amp-data-quality": "Profilage & Contrôle Qualité", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Ingestion de Profilage", "profiler-lowercase": "profilage", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Represent different persona that a user may have withing OpenMetadata.", "page-sub-header-for-pipelines": "Ingestion de métadonnées à partir des services de pipeline les plus utilisés.", "page-sub-header-for-policies": "Définissez des politiques avec un ensemble de règles pour un contrôle d'accès précis.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Attribuez des autorisations basées sur les rôles aux utilisateurs ou aux équipes.", "page-sub-header-for-search": "Ingestion de métadonnées à partir des services de recherche les plus populaires.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 26c7f30ca29..d679bbecf8b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "קונפיגורציית מטא-דאטה ל-Elasticsearch (אופציונלי)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "סוג מדד", "metric-value": "ערך מדד", "metrics-summary": "סיכום מדדים", @@ -845,6 +846,7 @@ "profile-sample-type": "דוגמת פרופיל {{type}}", "profiler": "מדד ואיכות נתונים", "profiler-amp-data-quality": "מדד ואיכות נתונים", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "הזנת מדד ואיכות נתונים", "profiler-lowercase": "מדד ואיכות נתונים", "profiler-setting-plural": "הגדרות מדד ואיכות נתונים", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "צור פרופיל משתמש (פרסונה) על מנת לשייך את המשתמש לפרופיל ולהתאים את הממשק לצרכים של המשתמשים כאשר הם נכנסים ל-UI.", "page-sub-header-for-pipelines": "שלב מטה-דאטה ממוצרי טעינת נתונים (ETL, ELT) פופלריים (כגון Airflow, Glue וכד׳)", "page-sub-header-for-policies": "הגדר מדיניות עם סט של כללים לבקרת גישה ברמת רזולוציה נמוכה.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "הקצאת גישה מבוססת תפקיד למשתמשים או קבוצות.", "page-sub-header-for-search": "שלב מטה-דאטה משירותי החיפוש הפופולריים ביותר.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 8ea0b6ad832..589dc0c5684 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Metadata To ES Config (Optional)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "メトリクスのタイプ", "metric-value": "メトリクスの値", "metrics-summary": "メトリクスの要約", @@ -845,6 +846,7 @@ "profile-sample-type": "サンプル{{type}}のプロファイル", "profiler": "プロファイラ", "profiler-amp-data-quality": "プロファイラとデータ品質", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "プロファイラのインジェスチョン", "profiler-lowercase": "プロファイラ", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Represent different persona that a user may have withing OpenMetadata.", "page-sub-header-for-pipelines": "Ingest metadata from the most used pipeline services.", "page-sub-header-for-policies": "Define policies with a set of rules for fine-grained access control.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Assign comprehensive role based access to Users or Teams.", "page-sub-header-for-search": "Ingest metadata from the most popular search services.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 541651bbb84..9c39cb8a9a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Metadata naar ES-configuratie (Optioneel)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Door Metapilot voorgestelde beschrijving", + "metric-configuration": "Metric Configuration", "metric-type": "Type metriek", "metric-value": "Metriekwaarde", "metrics-summary": "Samenvatting van metingen", @@ -845,6 +846,7 @@ "profile-sample-type": "Voorbeeldprofiel {{type}}", "profiler": "Profiler", "profiler-amp-data-quality": "Profiler & datakwaliteit", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Profileringestie", "profiler-lowercase": "profiler", "profiler-setting-plural": "Profilerinstellingen", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "De gebruikerservaring verbeteren en aanpassen met Persona's.", "page-sub-header-for-pipelines": "Ingest metadata van de meestgebruikte pipelineservices.", "page-sub-header-for-policies": "Definieer beleid met een reeks regels voor fijnmazige toegangscontrole.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Wijs uitgebreide rolgebaseerde toegang toe aan gebruikers of teams.", "page-sub-header-for-search": "Ingest metadata van de meestgebruikte zoekservices.", "page-sub-header-for-setting": "Mogelijkheid om de OpenMetadata-toepassing naar eigen behoefte te configureren.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index a23548fb9f7..f311e12bbff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Metadados para Configuração ES (Opcional)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "Tipo de Métrica", "metric-value": "Valor da Métrica", "metrics-summary": "Resumo de Métricas", @@ -845,6 +846,7 @@ "profile-sample-type": "Amostra de Perfil {{type}}", "profiler": "Profiler", "profiler-amp-data-quality": "Profiler & Qualidade de Dados", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Ingestão de Profiler", "profiler-lowercase": "profiler", "profiler-setting-plural": "Configurações do Profiler", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Crie Personas para associar a persona do usuário ao OpenMetadata", "page-sub-header-for-pipelines": "Ingestão de metadados dos serviços de pipeline mais utilizados.", "page-sub-header-for-policies": "Defina políticas com um conjunto de regras para controle de acesso detalhado.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Atribua acesso baseado em funções abrangentes a usuários ou equipes.", "page-sub-header-for-search": "Ingestão de metadados dos serviços de pesquisa mais populares.", "page-sub-header-for-setting": "Habilidade para configurar a aplicação OpenMetadata de acordo com suas necessidades.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 979c589aa5f..33e451780ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "Метаданные для конфигурации ES (необязательно)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "Тип метрики", "metric-value": "Значение метрики", "metrics-summary": "Сводка метрик", @@ -845,6 +846,7 @@ "profile-sample-type": "Образец профиля {{type}}", "profiler": "Профайлер", "profiler-amp-data-quality": "Профайлер & Качество данных", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "Получение профайлера", "profiler-lowercase": "профайлер", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Represent different persona that a user may have withing OpenMetadata.", "page-sub-header-for-pipelines": "Принимать метаданные из наиболее часто используемых конвейерных служб.", "page-sub-header-for-policies": "Определите политики с набором правил для точного контроля доступа.", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "Назначьте полный доступ на основе ролей пользователям или командам.", "page-sub-header-for-search": "Ingest metadata from the most popular search services.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index e3eba4be185..65a228232e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -677,6 +677,7 @@ "metadata-to-es-config-optional": "元数据到 ES 配置(可选)", "metapilot": "MetaPilot", "metapilot-suggested-description": "Metapilot Suggested Description", + "metric-configuration": "Metric Configuration", "metric-type": "指标类型", "metric-value": "指标值", "metrics-summary": "指标概要", @@ -845,6 +846,7 @@ "profile-sample-type": "分析样本{{type}}", "profiler": "分析器", "profiler-amp-data-quality": "数据分析质控", + "profiler-configuration": "Profiler Configuration", "profiler-ingestion": "分析器提取", "profiler-lowercase": "分析器", "profiler-setting-plural": "Profiler Settings", @@ -1628,6 +1630,7 @@ "page-sub-header-for-persona": "Represent different persona that a user may have withing OpenMetadata.", "page-sub-header-for-pipelines": "从最常用的工作流类型服务中提取元数据", "page-sub-header-for-policies": "通过组合一系列规则定义权限策略以精细化控制访问权限", + "page-sub-header-for-profiler-configuration": "Customize globally the behavior of the profiler by setting the metrics to compute based on columns data types", "page-sub-header-for-roles": "分配基于角色的访问权限给用户或团队", "page-sub-header-for-search": "Ingest metadata from the most popular search services.", "page-sub-header-for-setting": "Ability to configure the OpenMetadata application to suit your needs.", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.test.tsx new file mode 100644 index 00000000000..d360ca9e69a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2024 Collate. + * Licensed 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 { act, render, screen } from '@testing-library/react'; +import React from 'react'; +import { SettingType } from '../../generated/settings/settings'; +import { getSettingsConfigFromConfigType } from '../../rest/settingConfigAPI'; +import ProfilerConfigurationPage from './ProfilerConfigurationPage'; + +const mockHistory = { + goBack: jest.fn(), +}; + +jest.mock('../../components/common/Loader/Loader', () => + jest.fn().mockReturnValue(
Loading...
) +); +jest.mock( + '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component', + () => jest.fn().mockReturnValue(
TitleBreadcrumb.component
) +); +jest.mock('../../components/PageHeader/PageHeader.component', () => + jest.fn().mockReturnValue(
PageHeader.component
) +); +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn().mockImplementation(() => mockHistory), +})); +jest.mock('../../rest/settingConfigAPI', () => ({ + getSettingsConfigFromConfigType: jest.fn().mockResolvedValue({}), + updateSettingsConfig: jest.fn(), +})); +jest.mock('../../utils/GlobalSettingsUtils', () => ({ + getSettingPageEntityBreadCrumb: jest.fn().mockReturnValue([]), +})); +jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => + jest.fn().mockImplementation(({ children }) =>
{children}
) +); +jest.mock('../../constants/profiler.constant', () => ({ + DEFAULT_PROFILER_CONFIG_VALUE: { + metricConfiguration: [ + { + dataType: undefined, + metrics: undefined, + disabled: false, + }, + ], + }, + PROFILER_METRICS_TYPE_OPTIONS: [], +})); + +describe('ProfilerConfigurationPage', () => { + beforeEach(() => { + act(() => { + render(); + }); + }); + + it('renders the page correctly', async () => { + expect( + await screen.findByText('TitleBreadcrumb.component') + ).toBeInTheDocument(); + expect(await screen.findByText('PageHeader.component')).toBeInTheDocument(); + expect(await screen.findByText('label.data-type')).toBeInTheDocument(); + expect(await screen.findByText('label.disable')).toBeInTheDocument(); + expect(await screen.findByText('label.metric-type')).toBeInTheDocument(); + expect( + await screen.findByTestId('profiler-config-form') + ).toBeInTheDocument(); + expect(await screen.findByTestId('data-type-select')).toBeInTheDocument(); + expect(await screen.findByTestId('metric-type-select')).toBeInTheDocument(); + expect(await screen.findByTestId('disabled-switch')).toBeInTheDocument(); + expect(await screen.findByTestId('add-fields')).toBeInTheDocument(); + expect(await screen.findByTestId('cancel-button')).toBeInTheDocument(); + expect(await screen.findByTestId('save-button')).toBeInTheDocument(); + }); + + it('should fetch the profiler config data on initial render', () => { + const mockGetSettingsConfigFromConfigType = + getSettingsConfigFromConfigType as jest.Mock; + + expect(mockGetSettingsConfigFromConfigType).toHaveBeenCalledWith( + SettingType.ProfilerConfiguration + ); + expect(mockGetSettingsConfigFromConfigType).toHaveBeenCalledTimes(1); + }); + + it("onCancel should call history's goBack method", () => { + const cancelButton = screen.getByTestId('cancel-button'); + act(() => { + cancelButton.click(); + }); + + expect(mockHistory.goBack).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx new file mode 100644 index 00000000000..ae12cd2c7b6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx @@ -0,0 +1,300 @@ +/* + * Copyright 2024 Collate. + * Licensed 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 { CloseOutlined } from '@ant-design/icons'; +import { + Button, + Card, + Col, + Form, + Row, + Select, + Switch, + TreeSelect, + Typography, +} from 'antd'; +import { AxiosError } from 'axios'; +import { t } from 'i18next'; +import { isEmpty, isEqual, values } from 'lodash'; +import React, { Fragment, useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import Loader from '../../components/common/Loader/Loader'; +import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component'; +import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface'; +import PageHeader from '../../components/PageHeader/PageHeader.component'; +import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; +import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; +import { + DEFAULT_PROFILER_CONFIG_VALUE, + PROFILER_METRICS_TYPE_OPTIONS, +} from '../../constants/profiler.constant'; +import { + DataType, + MetricConfigurationDefinition, + MetricType, + ProfilerConfiguration, +} from '../../generated/configuration/profilerConfiguration'; +import { SettingType } from '../../generated/settings/settings'; +import { + getSettingsConfigFromConfigType, + updateSettingsConfig, +} from '../../rest/settingConfigAPI'; +import { getSettingPageEntityBreadCrumb } from '../../utils/GlobalSettingsUtils'; +import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; +import './profiler-configuration-page.style.less'; + +const ProfilerConfigurationPage = () => { + const [form] = Form.useForm(); + const history = useHistory(); + const [isLoading, setIsLoading] = useState(true); + const [isFormSubmitting, setIsFormSubmitting] = useState(false); + const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( + () => + getSettingPageEntityBreadCrumb( + GlobalSettingsMenuCategory.PREFERENCES, + t('label.profiler-configuration') + ), + [] + ); + + // Watchers + const selectedMetricConfiguration = Form.useWatch< + MetricConfigurationDefinition[] + >('metricConfiguration', form); + + const dataTypeOptions = useMemo(() => { + return values(DataType).map((value) => ({ + label: value, + key: value, + value, + // Disable the metric type selection if the data type is already selected + disabled: selectedMetricConfiguration?.some( + (data) => data?.dataType === value + ), + })); + }, [selectedMetricConfiguration]); + + const handleSubmit = async (data: ProfilerConfiguration) => { + setIsFormSubmitting(true); + const metricConfiguration = data.metricConfiguration?.map((item) => { + if (isEqual(item.metrics, ['all'])) { + return { + ...item, + // If all metrics are selected, then we will send full array of metrics + metrics: values(MetricType), + }; + } + + return item; + }); + try { + await updateSettingsConfig({ + config_type: SettingType.ProfilerConfiguration, + config_value: { + metricConfiguration, + }, + }); + showSuccessToast( + t('server.update-entity-success', { + entity: t('label.profiler-configuration'), + }) + ); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsFormSubmitting(false); + } + }; + + const fetchProfilerConfiguration = async () => { + setIsLoading(true); + try { + const { data } = await getSettingsConfigFromConfigType( + SettingType.ProfilerConfiguration + ); + + form.setFieldsValue( + isEmpty(data?.config_value?.metricConfiguration) + ? DEFAULT_PROFILER_CONFIG_VALUE + : data?.config_value + ); + } catch (error) { + // do nothing + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchProfilerConfiguration(); + }, []); + + if (isLoading) { + return ; + } + + return ( + + + + + + + + + + + + data-testid="profiler-config-form" + form={form} + id="profiler-config" + layout="vertical" + onFinish={handleSubmit}> + + {(fields, { add, remove }) => { + return ( + + + + {t('label.add-entity', { + entity: t('label.metric-configuration'), + })} + + + + {t('label.data-type')} + * + + {t('label.metric-type')} + {t('label.disable')} + {fields.map(({ key, name }) => ( + + + +